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

View File

@ -1,55 +1,62 @@
"""Extend the basic Accessory and Bridge functions.""" """Extend the basic Accessory and Bridge functions."""
import logging
from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory import Accessory, Bridge, Category
from .const import ( from .const import (
SERVICES_ACCESSORY_INFO, MANUFACTURER, SERV_ACCESSORY_INFO, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) 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 HomeAccessory(Accessory):
"""Class to extend the Accessory class.""" """Class to extend the Accessory class."""
ALL_CATEGORIES = Category def __init__(self, display_name, model, category='OTHER'):
def __init__(self, display_name):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(display_name) super().__init__(display_name)
set_accessory_info(self, model)
def set_category(self, category): self.category = getattr(Category, category, Category.OTHER)
"""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)
class HomeBridge(Bridge): class HomeBridge(Bridge):
"""Class to extend the Bridge class.""" """Class to extend the Bridge class."""
def __init__(self, display_name, pincode): def __init__(self, display_name, model, pincode):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode) super().__init__(display_name, pincode=pincode)
set_accessory_info(self, model)
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)

View File

@ -2,17 +2,20 @@
MANUFACTURER = 'HomeAssistant' MANUFACTURER = 'HomeAssistant'
# Service: AccessoryInfomation # Service: AccessoryInfomation
SERVICES_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
CHAR_MODEL = 'Model' CHAR_MODEL = 'Model'
CHAR_MANUFACTURER = 'Manufacturer' CHAR_MANUFACTURER = 'Manufacturer'
CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SERIAL_NUMBER = 'SerialNumber'
# Service: TemperatureSensor # Service: TemperatureSensor
SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
# Service: WindowCovering # Service: WindowCovering
SERVICES_WINDOW_COVERING = 'WindowCovering' SERV_WINDOW_COVERING = 'WindowCovering'
CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_POSITION_STATE = 'PositionState' 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 homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory, add_preload_service
from .const import ( from .const import (
SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION, SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE) CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
@ -23,10 +23,7 @@ class Window(HomeAccessory):
def __init__(self, hass, entity_id, display_name): def __init__(self, hass, entity_id, display_name):
"""Initialize a Window accessory object.""" """Initialize a Window accessory object."""
super().__init__(display_name) super().__init__(display_name, entity_id, 'WINDOW')
self.set_category(self.ALL_CATEGORIES.WINDOW)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_WINDOW_COVERING)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
@ -34,12 +31,12 @@ class Window(HomeAccessory):
self.current_position = None self.current_position = None
self.homekit_target = None self.homekit_target = None
self.service_cover = self.get_service(SERVICES_WINDOW_COVERING) self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.service_cover. \ self.char_current_position = self.serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION) get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.service_cover. \ self.char_target_position = self.serv_cover. \
get_characteristic(CHAR_TARGET_POSITION) get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.service_cover. \ self.char_position_state = self.serv_cover. \
get_characteristic(CHAR_POSITION_STATE) get_characteristic(CHAR_POSITION_STATE)
self.char_target_position.setter_callback = self.move_cover 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) self._hass, self._entity_id, self.update_cover_position)
def move_cover(self, value): 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: if value != self.current_position:
_LOGGER.debug("%s: Set position to %d", self._entity_id, value) _LOGGER.debug("%s: Set position to %d", self._entity_id, value)
self.homekit_target = value self.homekit_target = value

View File

@ -1,38 +1,55 @@
"""Class to hold all sensor accessories.""" """Class to hold all sensor accessories."""
import logging 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 homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import ( from .const import (
SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE) SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
_LOGGER = logging.getLogger(__name__) _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') @TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory): class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor. """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): def __init__(self, hass, entity_id, display_name):
"""Initialize a TemperatureSensor accessory object.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(display_name) super().__init__(display_name, entity_id, 'SENSOR')
self.set_category(self.ALL_CATEGORIES.SENSOR)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR) self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.service_temp. \ self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE) get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.unit = None
def run(self): def run(self):
"""Method called be object after driver is started.""" """Method called be object after driver is started."""
@ -48,6 +65,9 @@ class TemperatureSensor(HomeAccessory):
if new_state is None: if new_state is None:
return return
temperature = new_state.state unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
if temperature != STATE_UNKNOWN: temperature = calc_temperature(new_state.state, unit)
self.char_temp.set_value(float(temperature)) 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 DoorBirdPy==0.1.2
# homeassistant.components.homekit # homeassistant.components.homekit
HAP-python==1.1.5 HAP-python==1.1.7
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.1.0 PyISY==1.1.0

View File

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

View File

@ -1,13 +1,25 @@
"""Test different accessory types: Sensors.""" """Test different accessory types: Sensors."""
import unittest import unittest
from homeassistant.components.homekit.sensors import TemperatureSensor from homeassistant.components.homekit.sensors import (
TemperatureSensor, calc_temperature)
from homeassistant.const import ( 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 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): class TestHomekitSensors(unittest.TestCase):
"""Test class for all accessory types regarding sensors.""" """Test class for all accessory types regarding sensors."""
@ -16,7 +28,7 @@ class TestHomekitSensors(unittest.TestCase):
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
def tearDown(self): def tearDown(self):
"""Stop down everthing that was started.""" """Stop down everything that was started."""
self.hass.stop() self.hass.stop()
def test_temperature_celsius(self): def test_temperature_celsius(self):
@ -32,6 +44,12 @@ class TestHomekitSensors(unittest.TestCase):
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done() 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.hass.block_till_done()
self.assertEqual(acc.char_temp.value, 20) 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)