2018-02-19 22:46:22 +00:00
|
|
|
"""Extend the basic Accessory and Bridge functions."""
|
2018-04-06 21:11:53 +00:00
|
|
|
from datetime import timedelta
|
2018-05-30 10:39:27 +00:00
|
|
|
from functools import partial, wraps
|
2018-04-06 21:11:53 +00:00
|
|
|
from inspect import getmodule
|
2018-02-26 03:27:40 +00:00
|
|
|
import logging
|
|
|
|
|
2018-05-04 14:46:00 +00:00
|
|
|
from pyhap.accessory import Accessory, Bridge
|
2018-03-15 01:48:21 +00:00
|
|
|
from pyhap.accessory_driver import AccessoryDriver
|
2018-05-04 14:46:00 +00:00
|
|
|
from pyhap.const import CATEGORY_OTHER
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-06-17 18:54:34 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_BATTERY_CHARGING,
|
|
|
|
ATTR_BATTERY_LEVEL,
|
|
|
|
ATTR_ENTITY_ID,
|
|
|
|
ATTR_SERVICE,
|
|
|
|
__version__,
|
|
|
|
)
|
2019-02-14 15:01:46 +00:00
|
|
|
from homeassistant.core import callback as ha_callback, split_entity_id
|
2018-04-06 21:11:53 +00:00
|
|
|
from homeassistant.helpers.event import (
|
2019-07-31 19:25:30 +00:00
|
|
|
async_track_state_change,
|
|
|
|
track_point_in_utc_time,
|
|
|
|
)
|
2018-04-06 21:11:53 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
2018-03-16 00:05:28 +00:00
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
from .const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_DISPLAY_NAME,
|
|
|
|
ATTR_VALUE,
|
|
|
|
BRIDGE_MODEL,
|
|
|
|
BRIDGE_SERIAL_NUMBER,
|
|
|
|
CHAR_BATTERY_LEVEL,
|
|
|
|
CHAR_CHARGING_STATE,
|
|
|
|
CHAR_STATUS_LOW_BATTERY,
|
|
|
|
CONF_LINKED_BATTERY_SENSOR,
|
|
|
|
CONF_LOW_BATTERY_THRESHOLD,
|
|
|
|
DEBOUNCE_TIMEOUT,
|
|
|
|
DEFAULT_LOW_BATTERY_THRESHOLD,
|
|
|
|
EVENT_HOMEKIT_CHANGED,
|
|
|
|
MANUFACTURER,
|
|
|
|
SERV_BATTERY_SERVICE,
|
|
|
|
)
|
2019-02-14 15:01:46 +00:00
|
|
|
from .util import convert_to_float, dismiss_setup_message, show_setup_message
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-02-26 03:27:40 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
|
2018-04-06 21:11:53 +00:00
|
|
|
def debounce(func):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Decorate function to debounce callbacks from HomeKit."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-04-11 20:24:14 +00:00
|
|
|
@ha_callback
|
2018-05-30 10:39:27 +00:00
|
|
|
def call_later_listener(self, *args):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle call_later callback."""
|
2018-05-30 10:39:27 +00:00
|
|
|
debounce_params = self.debounce.pop(func.__name__, None)
|
|
|
|
if debounce_params:
|
2018-10-19 22:14:05 +00:00
|
|
|
self.hass.async_add_executor_job(func, self, *debounce_params[1:])
|
2018-04-06 21:11:53 +00:00
|
|
|
|
|
|
|
@wraps(func)
|
2018-05-30 10:39:27 +00:00
|
|
|
def wrapper(self, *args):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Start async timer."""
|
2018-05-30 10:39:27 +00:00
|
|
|
debounce_params = self.debounce.pop(func.__name__, None)
|
|
|
|
if debounce_params:
|
|
|
|
debounce_params[0]() # remove listener
|
2018-04-06 21:11:53 +00:00
|
|
|
remove_listener = track_point_in_utc_time(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass,
|
|
|
|
partial(call_later_listener, self),
|
|
|
|
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT),
|
|
|
|
)
|
2018-05-30 10:39:27 +00:00
|
|
|
self.debounce[func.__name__] = (remove_listener, *args)
|
2019-07-31 19:25:30 +00:00
|
|
|
logger.debug(
|
|
|
|
"%s: Start %s timeout", self.entity_id, func.__name__.replace("set_", "")
|
|
|
|
)
|
2018-04-06 21:11:53 +00:00
|
|
|
|
|
|
|
name = getmodule(func).__name__
|
|
|
|
logger = logging.getLogger(name)
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
2018-02-26 03:27:40 +00:00
|
|
|
class HomeAccessory(Accessory):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Adapter class for Accessory."""
|
2018-02-26 03:27:40 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER
|
|
|
|
):
|
2018-02-26 03:27:40 +00:00
|
|
|
"""Initialize a Accessory object."""
|
2018-05-29 20:43:26 +00:00
|
|
|
super().__init__(driver, name, aid=aid)
|
2018-05-11 12:22:45 +00:00
|
|
|
model = split_entity_id(entity_id)[0].replace("_", " ").title()
|
2018-05-04 14:46:00 +00:00
|
|
|
self.set_info_service(
|
2019-07-31 19:25:30 +00:00
|
|
|
firmware_revision=__version__,
|
|
|
|
manufacturer=MANUFACTURER,
|
|
|
|
model=model,
|
|
|
|
serial_number=entity_id,
|
|
|
|
)
|
2018-05-04 14:46:00 +00:00
|
|
|
self.category = category
|
2019-04-09 21:13:48 +00:00
|
|
|
self.config = config or {}
|
2018-04-11 20:24:14 +00:00
|
|
|
self.entity_id = entity_id
|
|
|
|
self.hass = hass
|
2018-05-30 10:39:27 +00:00
|
|
|
self.debounce = {}
|
2018-06-17 18:54:34 +00:00
|
|
|
self._support_battery_level = False
|
|
|
|
self._support_battery_charging = True
|
2019-07-31 19:25:30 +00:00
|
|
|
self.linked_battery_sensor = self.config.get(CONF_LINKED_BATTERY_SENSOR)
|
|
|
|
self.low_battery_threshold = self.config.get(
|
|
|
|
CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD
|
|
|
|
)
|
2018-06-17 18:54:34 +00:00
|
|
|
|
|
|
|
"""Add battery service if available"""
|
2019-07-31 19:25:30 +00:00
|
|
|
battery_found = self.hass.states.get(self.entity_id).attributes.get(
|
|
|
|
ATTR_BATTERY_LEVEL
|
|
|
|
)
|
2019-04-09 21:13:48 +00:00
|
|
|
if self.linked_battery_sensor:
|
2019-07-31 19:25:30 +00:00
|
|
|
battery_found = self.hass.states.get(self.linked_battery_sensor).state
|
2019-04-09 21:13:48 +00:00
|
|
|
|
|
|
|
if battery_found is None:
|
2018-06-17 18:54:34 +00:00
|
|
|
return
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("%s: Found battery level", self.entity_id)
|
2018-06-17 18:54:34 +00:00
|
|
|
self._support_battery_level = True
|
|
|
|
serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE)
|
2019-07-31 19:25:30 +00:00
|
|
|
self._char_battery = serv_battery.configure_char(CHAR_BATTERY_LEVEL, value=0)
|
|
|
|
self._char_charging = serv_battery.configure_char(CHAR_CHARGING_STATE, value=2)
|
2018-06-17 18:54:34 +00:00
|
|
|
self._char_low_battery = serv_battery.configure_char(
|
2019-07-31 19:25:30 +00:00
|
|
|
CHAR_STATUS_LOW_BATTERY, value=0
|
|
|
|
)
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-05-30 10:39:27 +00:00
|
|
|
async def run(self):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle accessory driver started event.
|
2018-05-30 10:39:27 +00:00
|
|
|
|
|
|
|
Run inside the HAP-python event loop.
|
|
|
|
"""
|
2018-10-19 22:14:05 +00:00
|
|
|
self.hass.add_job(self.run_handler)
|
|
|
|
|
|
|
|
async def run_handler(self):
|
|
|
|
"""Handle accessory driver started event.
|
|
|
|
|
|
|
|
Run inside the Home Assistant event loop.
|
|
|
|
"""
|
2018-04-04 22:52:25 +00:00
|
|
|
state = self.hass.states.get(self.entity_id)
|
2018-10-19 22:14:05 +00:00
|
|
|
self.hass.async_add_job(self.update_state_callback, None, None, state)
|
2019-07-31 19:25:30 +00:00
|
|
|
async_track_state_change(self.hass, self.entity_id, self.update_state_callback)
|
2018-04-11 20:24:14 +00:00
|
|
|
|
2019-04-09 21:13:48 +00:00
|
|
|
if self.linked_battery_sensor:
|
|
|
|
battery_state = self.hass.states.get(self.linked_battery_sensor)
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.async_add_job(
|
|
|
|
self.update_linked_battery, None, None, battery_state
|
|
|
|
)
|
2019-04-09 21:13:48 +00:00
|
|
|
async_track_state_change(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass, self.linked_battery_sensor, self.update_linked_battery
|
|
|
|
)
|
2019-04-09 21:13:48 +00:00
|
|
|
|
2018-05-21 02:25:53 +00:00
|
|
|
@ha_callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def update_state_callback(self, entity_id=None, old_state=None, new_state=None):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle state change listener callback."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("New_state: %s", new_state)
|
2018-04-11 20:24:14 +00:00
|
|
|
if new_state is None:
|
|
|
|
return
|
2019-04-09 21:13:48 +00:00
|
|
|
if self._support_battery_level and not self.linked_battery_sensor:
|
2018-10-19 22:14:05 +00:00
|
|
|
self.hass.async_add_executor_job(self.update_battery, new_state)
|
|
|
|
self.hass.async_add_executor_job(self.update_state, new_state)
|
2018-04-11 20:24:14 +00:00
|
|
|
|
2019-04-09 21:13:48 +00:00
|
|
|
@ha_callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def update_linked_battery(self, entity_id=None, old_state=None, new_state=None):
|
2019-04-09 21:13:48 +00:00
|
|
|
"""Handle linked battery sensor state change listener callback."""
|
|
|
|
self.hass.async_add_executor_job(self.update_battery, new_state)
|
|
|
|
|
2018-06-17 18:54:34 +00:00
|
|
|
def update_battery(self, new_state):
|
|
|
|
"""Update battery service if available.
|
|
|
|
|
|
|
|
Only call this function if self._support_battery_level is True.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
battery_level = convert_to_float(new_state.attributes.get(ATTR_BATTERY_LEVEL))
|
2019-04-09 21:13:48 +00:00
|
|
|
if self.linked_battery_sensor:
|
|
|
|
battery_level = convert_to_float(new_state.state)
|
2019-02-17 07:04:29 +00:00
|
|
|
if battery_level is None:
|
|
|
|
return
|
2018-06-17 18:54:34 +00:00
|
|
|
self._char_battery.set_value(battery_level)
|
2019-07-31 19:25:30 +00:00
|
|
|
self._char_low_battery.set_value(battery_level < self.low_battery_threshold)
|
|
|
|
_LOGGER.debug("%s: Updated battery level to %d", self.entity_id, battery_level)
|
2018-06-17 18:54:34 +00:00
|
|
|
if not self._support_battery_charging:
|
|
|
|
return
|
|
|
|
charging = new_state.attributes.get(ATTR_BATTERY_CHARGING)
|
|
|
|
if charging is None:
|
|
|
|
self._support_battery_charging = False
|
|
|
|
return
|
|
|
|
hk_charging = 1 if charging is True else 0
|
|
|
|
self._char_charging.set_value(hk_charging)
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("%s: Updated battery charging to %d", self.entity_id, hk_charging)
|
2018-06-17 18:54:34 +00:00
|
|
|
|
2018-04-11 20:24:14 +00:00
|
|
|
def update_state(self, new_state):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle state change to update HomeKit value.
|
2018-04-11 20:24:14 +00:00
|
|
|
|
|
|
|
Overridden by accessory types.
|
|
|
|
"""
|
2018-05-21 02:25:53 +00:00
|
|
|
raise NotImplementedError()
|
2018-03-16 00:05:28 +00:00
|
|
|
|
2018-10-16 11:32:53 +00:00
|
|
|
def call_service(self, domain, service, service_data, value=None):
|
|
|
|
"""Fire event and call service for changes from HomeKit."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.add_job(self.async_call_service, domain, service, service_data, value)
|
2018-10-16 11:32:53 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_call_service(self, domain, service, service_data, value=None):
|
2018-10-16 11:32:53 +00:00
|
|
|
"""Fire event and call service for changes from HomeKit.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
event_data = {
|
|
|
|
ATTR_ENTITY_ID: self.entity_id,
|
|
|
|
ATTR_DISPLAY_NAME: self.display_name,
|
|
|
|
ATTR_SERVICE: service,
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_VALUE: value,
|
2018-10-16 11:32:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data)
|
|
|
|
await self.hass.services.async_call(domain, service, service_data)
|
|
|
|
|
2018-02-19 22:46:22 +00:00
|
|
|
|
|
|
|
class HomeBridge(Bridge):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Adapter class for Bridge."""
|
2018-02-19 22:46:22 +00:00
|
|
|
|
2018-07-22 07:51:42 +00:00
|
|
|
def __init__(self, hass, driver, name):
|
2018-02-19 22:46:22 +00:00
|
|
|
"""Initialize a Bridge object."""
|
2018-05-29 20:43:26 +00:00
|
|
|
super().__init__(driver, name)
|
2018-05-04 14:46:00 +00:00
|
|
|
self.set_info_service(
|
2019-07-31 19:25:30 +00:00
|
|
|
firmware_revision=__version__,
|
|
|
|
manufacturer=MANUFACTURER,
|
|
|
|
model=BRIDGE_MODEL,
|
|
|
|
serial_number=BRIDGE_SERIAL_NUMBER,
|
|
|
|
)
|
2018-04-04 22:52:25 +00:00
|
|
|
self.hass = hass
|
2018-03-01 23:20:02 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def setup_message(self):
|
|
|
|
"""Prevent print of pyhap setup message to terminal."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class HomeDriver(AccessoryDriver):
|
|
|
|
"""Adapter class for AccessoryDriver."""
|
|
|
|
|
2018-05-29 20:43:26 +00:00
|
|
|
def __init__(self, hass, **kwargs):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Initialize a AccessoryDriver object."""
|
2018-05-29 20:43:26 +00:00
|
|
|
super().__init__(**kwargs)
|
2018-05-18 14:32:57 +00:00
|
|
|
self.hass = hass
|
|
|
|
|
|
|
|
def pair(self, client_uuid, client_public):
|
|
|
|
"""Override super function to dismiss setup message if paired."""
|
2018-05-30 10:39:27 +00:00
|
|
|
success = super().pair(client_uuid, client_public)
|
|
|
|
if success:
|
2018-05-18 14:32:57 +00:00
|
|
|
dismiss_setup_message(self.hass)
|
2018-05-30 10:39:27 +00:00
|
|
|
return success
|
2018-05-18 14:32:57 +00:00
|
|
|
|
|
|
|
def unpair(self, client_uuid):
|
|
|
|
"""Override super function to show setup message if unpaired."""
|
|
|
|
super().unpair(client_uuid)
|
|
|
|
show_setup_message(self.hass, self.state.pincode)
|