Homekit Update, Support for TempSensor (°F) (#12676)

* Changed version of "HAP-python" to "v1.1.7"

* Updated acc file to simplify init calls

* Code refactored and '°F' temp Sensors added

* Changed call to 'HomeAccessory' and 'HomeBridge'
* Extended function of 'add_preload_service' to add additional characteristics
* Added function to override characteristic property values

* TemperatureSensor
  * Added unit
  * Added calc_temperature

* Updated tests
This commit is contained in:
cdce8p 2018-02-26 04:27:40 +01:00 committed by Paulus Schoutsen
parent 347ba1a2d8
commit 27b1d448a3
8 changed files with 120 additions and 74 deletions

View file

@ -11,7 +11,8 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
TEMP_CELSIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
@ -21,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
_RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$")
DOMAIN = 'homekit'
REQUIREMENTS = ['HAP-python==1.1.5']
REQUIREMENTS = ['HAP-python==1.1.7']
BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode'
@ -74,7 +75,8 @@ def import_types():
def get_accessory(hass, state):
"""Take state and return an accessory object if supported."""
if state.domain == 'sensor':
if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS:
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug("Add \"%s\" as \"%s\"",
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
@ -103,8 +105,7 @@ class HomeKit():
def setup_bridge(self, pin):
"""Setup the bridge component to track all accessories."""
from .accessories import HomeBridge
self.bridge = HomeBridge(BRIDGE_NAME, pincode=pin)
self.bridge.set_accessory_info('homekit.bridge')
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
def start_driver(self, event):
"""Start the accessory driver."""

View file

@ -1,55 +1,62 @@
"""Extend the basic Accessory and Bridge functions."""
import logging
from pyhap.accessory import Accessory, Bridge, Category
from .const import (
SERVICES_ACCESSORY_INFO, MANUFACTURER,
SERV_ACCESSORY_INFO, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER)
_LOGGER = logging.getLogger(__name__)
def set_accessory_info(acc, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service = acc.get_service(SERV_ACCESSORY_INFO)
service.get_characteristic(CHAR_MODEL).set_value(model)
service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer)
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def add_preload_service(acc, service, chars=None, opt_chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
if opt_chars:
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
for opt_char_name in opt_chars:
opt_char = get_char_loader().get(opt_char_name)
service.add_opt_characteristic(opt_char)
acc.add_service(service)
return service
def override_properties(char, new_properties):
"""Override characteristic property values."""
char.properties.update(new_properties)
class HomeAccessory(Accessory):
"""Class to extend the Accessory class."""
ALL_CATEGORIES = Category
def __init__(self, display_name):
def __init__(self, display_name, model, category='OTHER'):
"""Initialize a Accessory object."""
super().__init__(display_name)
def set_category(self, category):
"""Set the category of the accessory."""
self.category = category
def add_preload_service(self, service):
"""Define the services to be available for the accessory."""
from pyhap.loader import get_serv_loader
self.add_service(get_serv_loader().get(service))
def set_accessory_info(self, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
service_info.get_characteristic(CHAR_MODEL) \
.set_value(model)
service_info.get_characteristic(CHAR_MANUFACTURER) \
.set_value(manufacturer)
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
.set_value(serial_number)
set_accessory_info(self, model)
self.category = getattr(Category, category, Category.OTHER)
class HomeBridge(Bridge):
"""Class to extend the Bridge class."""
def __init__(self, display_name, pincode):
def __init__(self, display_name, model, pincode):
"""Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode)
def set_accessory_info(self, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
service_info.get_characteristic(CHAR_MODEL) \
.set_value(model)
service_info.get_characteristic(CHAR_MANUFACTURER) \
.set_value(manufacturer)
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
.set_value(serial_number)
set_accessory_info(self, model)

View file

@ -2,17 +2,20 @@
MANUFACTURER = 'HomeAssistant'
# Service: AccessoryInfomation
SERVICES_ACCESSORY_INFO = 'AccessoryInformation'
SERV_ACCESSORY_INFO = 'AccessoryInformation'
CHAR_MODEL = 'Model'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_SERIAL_NUMBER = 'SerialNumber'
# Service: TemperatureSensor
SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
# Service: WindowCovering
SERVICES_WINDOW_COVERING = 'WindowCovering'
SERV_WINDOW_COVERING = 'WindowCovering'
CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_POSITION_STATE = 'PositionState'
# Properties
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}

View file

@ -5,9 +5,9 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION,
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
@ -23,10 +23,7 @@ class Window(HomeAccessory):
def __init__(self, hass, entity_id, display_name):
"""Initialize a Window accessory object."""
super().__init__(display_name)
self.set_category(self.ALL_CATEGORIES.WINDOW)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_WINDOW_COVERING)
super().__init__(display_name, entity_id, 'WINDOW')
self._hass = hass
self._entity_id = entity_id
@ -34,12 +31,12 @@ class Window(HomeAccessory):
self.current_position = None
self.homekit_target = None
self.service_cover = self.get_service(SERVICES_WINDOW_COVERING)
self.char_current_position = self.service_cover. \
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.service_cover. \
self.char_target_position = self.serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.service_cover. \
self.char_position_state = self.serv_cover. \
get_characteristic(CHAR_POSITION_STATE)
self.char_target_position.setter_callback = self.move_cover
@ -53,7 +50,7 @@ class Window(HomeAccessory):
self._hass, self._entity_id, self.update_cover_position)
def move_cover(self, value):
"""Move cover to value if call came from homekit."""
"""Move cover to value if call came from HomeKit."""
if value != self.current_position:
_LOGGER.debug("%s: Set position to %d", self._entity_id, value)
self.homekit_target = value

View file

@ -1,38 +1,55 @@
"""Class to hold all sensor accessories."""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.const import (
STATE_UNKNOWN, ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory
from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import (
SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE)
SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
def calc_temperature(state, unit=TEMP_CELSIUS):
"""Calculate temperature from state and unit.
Always return temperature as Celsius value.
Conversion is handled on the device.
"""
if state == STATE_UNKNOWN:
return None
if unit == TEMP_FAHRENHEIT:
value = round((float(state) - 32) / 1.8, 2)
else:
value = float(state)
return value
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor.
Sensor entity must return either temperature in °C or STATE_UNKNOWN.
Sensor entity must return temperature in °C, °F or STATE_UNKNOWN.
"""
def __init__(self, hass, entity_id, display_name):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(display_name)
self.set_category(self.ALL_CATEGORIES.SENSOR)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
super().__init__(display_name, entity_id, 'SENSOR')
self._hass = hass
self._entity_id = entity_id
self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR)
self.char_temp = self.service_temp. \
self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.unit = None
def run(self):
"""Method called be object after driver is started."""
@ -48,6 +65,9 @@ class TemperatureSensor(HomeAccessory):
if new_state is None:
return
temperature = new_state.state
if temperature != STATE_UNKNOWN:
self.char_temp.set_value(float(temperature))
unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
temperature = calc_temperature(new_state.state, unit)
if temperature is not None:
self.char_temp.set_value(temperature)
_LOGGER.debug("%s: Current temperature set to %d°C",
self._entity_id, temperature)

View file

@ -24,7 +24,7 @@ attrs==17.4.0
DoorBirdPy==0.1.2
# homeassistant.components.homekit
HAP-python==1.1.5
HAP-python==1.1.7
# homeassistant.components.isy994
PyISY==1.1.0

View file

@ -19,7 +19,7 @@ asynctest>=0.11.1
# homeassistant.components.homekit
HAP-python==1.1.5
HAP-python==1.1.7
# homeassistant.components.notify.html5
PyJWT==1.5.3

View file

@ -1,13 +1,25 @@
"""Test different accessory types: Sensors."""
import unittest
from homeassistant.components.homekit.sensors import TemperatureSensor
from homeassistant.components.homekit.sensors import (
TemperatureSensor, calc_temperature)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN)
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN)
from tests.common import get_test_home_assistant
def test_calc_temperature():
"""Test if temperature in Celsius is calculated correctly."""
assert calc_temperature(STATE_UNKNOWN) is None
assert calc_temperature('20') == 20
assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12
assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24
assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22
class TestHomekitSensors(unittest.TestCase):
"""Test class for all accessory types regarding sensors."""
@ -16,7 +28,7 @@ class TestHomekitSensors(unittest.TestCase):
self.hass = get_test_home_assistant()
def tearDown(self):
"""Stop down everthing that was started."""
"""Stop down everything that was started."""
self.hass.stop()
def test_temperature_celsius(self):
@ -32,6 +44,12 @@ class TestHomekitSensors(unittest.TestCase):
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.hass.states.set(temperature_sensor, '20')
self.hass.states.set(temperature_sensor, '20',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
self.assertEqual(acc.char_temp.value, 20)
self.hass.states.set(temperature_sensor, '75.2',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT})
self.hass.block_till_done()
self.assertEqual(acc.char_temp.value, 24)