core/homeassistant/components/homekit/__init__.py

402 lines
12 KiB
Python
Raw Normal View History

"""Support for Apple HomeKit."""
import ipaddress
import logging
from zlib import adler32
import voluptuous as vol
from homeassistant.components import cover
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
CONF_IP_ADDRESS,
CONF_NAME,
CONF_PORT,
CONF_TYPE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
2019-07-31 19:25:30 +00:00
BRIDGE_NAME,
CONF_ADVERTISE_IP,
2019-07-31 19:25:30 +00:00
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
CONF_FEATURE_LIST,
CONF_FILTER,
CONF_SAFE_MODE,
DEFAULT_AUTO_START,
DEFAULT_PORT,
DEFAULT_SAFE_MODE,
DEVICE_CLASS_CO,
DEVICE_CLASS_CO2,
DEVICE_CLASS_PM25,
DOMAIN,
HOMEKIT_FILE,
SERVICE_HOMEKIT_START,
SERVICE_HOMEKIT_RESET_ACCESSORY,
TYPE_FAUCET,
TYPE_OUTLET,
TYPE_SHOWER,
TYPE_SPRINKLER,
TYPE_SWITCH,
TYPE_VALVE,
)
from .util import (
2019-07-31 19:25:30 +00:00
show_setup_message,
validate_entity_config,
validate_media_player_features,
)
_LOGGER = logging.getLogger(__name__)
MAX_DEVICES = 100
TYPES = Registry()
# #### Driver Status ####
STATUS_READY = 0
STATUS_RUNNING = 1
STATUS_STOPPED = 2
STATUS_WAIT = 3
SWITCH_TYPES = {
2019-07-31 19:25:30 +00:00
TYPE_FAUCET: "Valve",
TYPE_OUTLET: "Outlet",
TYPE_SHOWER: "Valve",
TYPE_SPRINKLER: "Valve",
TYPE_SWITCH: "Switch",
TYPE_VALVE: "Valve",
}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
{
vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All(
cv.string, vol.Length(min=3, max=25)
),
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_ADVERTISE_IP): vol.All(
ipaddress.ip_address, cv.string
),
2019-07-31 19:25:30 +00:00
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
}
)
},
extra=vol.ALLOW_EXTRA,
)
RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids}
)
async def async_setup(hass, config):
2018-08-19 20:29:08 +00:00
"""Set up the HomeKit component."""
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Begin setup HomeKit")
conf = config[DOMAIN]
name = conf[CONF_NAME]
port = conf[CONF_PORT]
ip_address = conf.get(CONF_IP_ADDRESS)
advertise_ip = conf.get(CONF_ADVERTISE_IP)
auto_start = conf[CONF_AUTO_START]
safe_mode = conf[CONF_SAFE_MODE]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
2019-07-31 19:25:30 +00:00
homekit = HomeKit(
hass,
name,
port,
ip_address,
entity_filter,
entity_config,
safe_mode,
advertise_ip,
2019-07-31 19:25:30 +00:00
)
await hass.async_add_executor_job(homekit.setup)
def handle_homekit_reset_accessory(service):
"""Handle start HomeKit service call."""
if homekit.status != STATUS_RUNNING:
_LOGGER.warning(
2019-07-31 19:25:30 +00:00
"HomeKit is not running. Either it is waiting to be "
"started or has been stopped."
)
return
2019-07-31 19:25:30 +00:00
entity_ids = service.data.get("entity_id")
homekit.reset_accessories(entity_ids)
2019-07-31 19:25:30 +00:00
hass.services.async_register(
DOMAIN,
SERVICE_HOMEKIT_RESET_ACCESSORY,
handle_homekit_reset_accessory,
schema=RESET_ACCESSORY_SERVICE_SCHEMA,
)
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
return True
def handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
if homekit.status != STATUS_READY:
_LOGGER.warning(
2019-07-31 19:25:30 +00:00
"HomeKit is not ready. Either it is already running or has "
"been stopped."
)
return
homekit.start()
2019-07-31 19:25:30 +00:00
hass.services.async_register(
DOMAIN, SERVICE_HOMEKIT_START, handle_homekit_service_start
)
return True
def get_accessory(hass, driver, state, aid, config):
"""Take state and return an accessory object if supported."""
if not aid:
2019-07-31 19:25:30 +00:00
_LOGGER.warning(
'The entity "%s" is not supported, since it '
"generates an invalid aid, please change it.",
state.entity_id,
)
return None
2018-04-11 20:24:14 +00:00
a_type = None
name = config.get(CONF_NAME, state.name)
2018-04-11 20:24:14 +00:00
2019-07-31 19:25:30 +00:00
if state.domain == "alarm_control_panel":
a_type = "SecuritySystem"
2019-07-31 19:25:30 +00:00
elif state.domain in ("binary_sensor", "device_tracker", "person"):
a_type = "BinarySensor"
2018-04-11 20:24:14 +00:00
2019-07-31 19:25:30 +00:00
elif state.domain == "climate":
a_type = "Thermostat"
2019-07-31 19:25:30 +00:00
elif state.domain == "cover":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
2018-06-17 11:37:44 +00:00
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
2019-07-31 19:25:30 +00:00
if device_class == "garage" and features & (
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
):
a_type = "GarageDoorOpener"
elif features & cover.SUPPORT_SET_POSITION:
2019-07-31 19:25:30 +00:00
a_type = "WindowCovering"
elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
2019-07-31 19:25:30 +00:00
a_type = "WindowCoveringBasic"
2019-07-31 19:25:30 +00:00
elif state.domain == "fan":
a_type = "Fan"
2018-05-16 11:15:59 +00:00
2019-07-31 19:25:30 +00:00
elif state.domain == "light":
a_type = "Light"
2019-07-31 19:25:30 +00:00
elif state.domain == "lock":
a_type = "Lock"
2018-04-11 20:24:14 +00:00
2019-07-31 19:25:30 +00:00
elif state.domain == "media_player":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
feature_list = config.get(CONF_FEATURE_LIST)
if device_class == DEVICE_CLASS_TV:
2019-07-31 19:25:30 +00:00
a_type = "TelevisionMediaPlayer"
else:
2019-07-31 19:25:30 +00:00
if feature_list and validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"
2019-07-31 19:25:30 +00:00
elif state.domain == "sensor":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
2018-06-17 11:37:44 +00:00
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
2019-07-31 19:25:30 +00:00
if device_class == DEVICE_CLASS_TEMPERATURE or unit in (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
):
a_type = "TemperatureSensor"
elif device_class == DEVICE_CLASS_HUMIDITY and unit == "%":
a_type = "HumiditySensor"
elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id:
a_type = "AirQualitySensor"
elif device_class == DEVICE_CLASS_CO:
2019-07-31 19:25:30 +00:00
a_type = "CarbonMonoxideSensor"
elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id:
a_type = "CarbonDioxideSensor"
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"):
a_type = "LightSensor"
elif state.domain == "switch":
switch_type = config.get(CONF_TYPE, TYPE_SWITCH)
a_type = SWITCH_TYPES[switch_type]
2019-07-31 19:25:30 +00:00
elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"):
a_type = "Switch"
2018-04-11 20:24:14 +00:00
2019-07-31 19:25:30 +00:00
elif state.domain == "water_heater":
a_type = "WaterHeater"
2018-04-11 20:24:14 +00:00
if a_type is None:
return None
2018-04-11 20:24:14 +00:00
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)
def generate_aid(entity_id):
"""Generate accessory aid with zlib adler32."""
2019-07-31 19:25:30 +00:00
aid = adler32(entity_id.encode("utf-8"))
if aid in (0, 1):
return None
return aid
2019-07-31 19:25:30 +00:00
class HomeKit:
"""Class to handle all actions between HomeKit and Home Assistant."""
2019-07-31 19:25:30 +00:00
def __init__(
self,
hass,
name,
port,
ip_address,
entity_filter,
entity_config,
safe_mode,
advertise_ip=None,
2019-07-31 19:25:30 +00:00
):
"""Initialize a HomeKit object."""
2018-04-11 20:24:14 +00:00
self.hass = hass
self._name = name
self._port = port
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
self._safe_mode = safe_mode
self._advertise_ip = advertise_ip
self.status = STATUS_READY
self.bridge = None
self.driver = None
def setup(self):
2018-08-19 20:29:08 +00:00
"""Set up bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
2019-07-31 19:25:30 +00:00
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
ip_addr = self._ip_address or get_local_ip()
2018-04-11 20:24:14 +00:00
path = self.hass.config.path(HOMEKIT_FILE)
2019-07-31 19:25:30 +00:00
self.driver = HomeDriver(
self.hass,
address=ip_addr,
port=self._port,
persist_file=path,
advertised_address=self._advertise_ip,
2019-07-31 19:25:30 +00:00
)
self.bridge = HomeBridge(self.hass, self.driver, self._name)
if self._safe_mode:
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Safe_mode selected")
self.driver.safe_mode = True
def reset_accessories(self, entity_ids):
"""Reset the accessory to load the latest configuration."""
removed = []
for entity_id in entity_ids:
aid = generate_aid(entity_id)
if aid not in self.bridge.accessories:
2019-07-31 19:25:30 +00:00
_LOGGER.warning(
"Could not reset accessory. entity_id " "not found %s", entity_id
)
continue
acc = self.remove_bridge_accessory(aid)
removed.append(acc)
self.driver.config_changed()
for acc in removed:
self.bridge.add_accessory(acc)
self.driver.config_changed()
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
if not state or not self._filter(state.entity_id):
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self.hass, self.driver, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
def remove_bridge_accessory(self, aid):
"""Try adding accessory to bridge if configured beforehand."""
acc = None
if aid in self.bridge.accessories:
acc = self.bridge.accessories.pop(aid)
return acc
def start(self, *args):
"""Start the accessory driver."""
if self.status != STATUS_READY:
return
self.status = STATUS_WAIT
from . import ( # noqa: F401 pylint: disable=unused-import
2019-07-31 19:25:30 +00:00
type_covers,
type_fans,
type_lights,
type_locks,
type_media_players,
type_security_systems,
type_sensors,
type_switches,
type_thermostats,
)
2018-04-11 20:24:14 +00:00
for state in self.hass.states.all():
self.add_bridge_accessory(state)
self.driver.add_accessory(self.bridge)
2018-05-18 14:32:57 +00:00
if not self.driver.state.paired:
show_setup_message(self.hass, self.driver.state.pincode)
if len(self.bridge.accessories) > MAX_DEVICES:
2019-07-31 19:25:30 +00:00
_LOGGER.warning(
"You have exceeded the device limit, which might "
"cause issues. Consider using the filter option."
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Driver start")
self.hass.add_job(self.driver.start)
self.status = STATUS_RUNNING
def stop(self, *args):
"""Stop the accessory driver."""
if self.status != STATUS_RUNNING:
return
self.status = STATUS_STOPPED
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Driver stop")
self.hass.add_job(self.driver.stop)