From bfa8cb760f41a820672371f261a9191dcb04589a Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 18 Jan 2020 12:14:50 +0000 Subject: [PATCH] Add services to geniushub (#30918) adds: - `set_zone_override`, and - `set_zone_mode` --- .../components/geniushub/__init__.py | 99 +++++++++++++++++-- .../components/geniushub/services.yaml | 29 ++++++ 2 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/geniushub/services.yaml diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 977656149c5..bb25d2d619d 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -8,6 +8,7 @@ from geniushubclient import GeniusHub import voluptuous as vol from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_HOST, CONF_MAC, @@ -26,11 +27,10 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -ATTR_DURATION = "duration" - _LOGGER = logging.getLogger(__name__) DOMAIN = "geniushub" @@ -68,6 +68,30 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) +ATTR_ZONE_MODE = "mode" +ATTR_DURATION = "duration" + +SVC_SET_ZONE_MODE = "set_zone_mode" +SVC_SET_ZONE_OVERRIDE = "set_zone_override" + +SET_ZONE_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ZONE_MODE): vol.In(["off", "timer", "footprint"]), + } +) +SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TEMPERATURE): vol.All( + vol.Coerce(float), vol.Range(min=4, max=28) + ), + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + ), + } +) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a Genius Hub system.""" @@ -96,9 +120,45 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: for platform in ["climate", "water_heater", "sensor", "binary_sensor", "switch"]: hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) + setup_service_functions(hass, broker) + return True +@callback +def setup_service_functions(hass: HomeAssistantType, broker): + """Set up the service functions.""" + + @verify_domain_control(hass, DOMAIN) + async def set_zone_mode(call) -> None: + """Set the system mode.""" + entity_id = call.data[ATTR_ENTITY_ID] + + registry = await hass.helpers.entity_registry.async_get_registry() + registry_entry = registry.async_get(entity_id) + + if registry_entry is None or registry_entry.platform != DOMAIN: + raise ValueError(f"'{entity_id}' is not a known {DOMAIN} entity") + + if registry_entry.domain != "climate": + raise ValueError(f"'{entity_id}' is not an {DOMAIN} zone") + + payload = { + "unique_id": registry_entry.unique_id, + "service": call.service, + "data": call.data, + } + + async_dispatcher_send(hass, DOMAIN, payload) + + hass.services.async_register( + DOMAIN, SVC_SET_ZONE_MODE, set_zone_mode, schema=SET_ZONE_MODE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SVC_SET_ZONE_OVERRIDE, set_zone_mode, schema=SET_ZONE_OVERRIDE_SCHEMA + ) + + class GeniusBroker: """Container for geniushub client and data.""" @@ -146,8 +206,8 @@ class GeniusEntity(Entity): """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - @callback - def _refresh(self) -> None: + async def _refresh(self, payload: Optional[dict] = None) -> None: + """Process any signals.""" self.async_schedule_update_ha_state(force_refresh=True) @property @@ -175,7 +235,6 @@ class GeniusDevice(GeniusEntity): self._device = device self._unique_id = f"{broker.hub_uid}_device_{device.id}" - self._last_comms = self._state_attr = None @property @@ -188,7 +247,7 @@ class GeniusDevice(GeniusEntity): attrs["last_comms"] = self._last_comms.isoformat() state = dict(self._device.data["state"]) - if "_state" in self._device.data: # only for v3 API + if "_state" in self._device.data: # only via v3 API state.update(self._device.data["_state"]) attrs["state"] = { @@ -199,7 +258,7 @@ class GeniusDevice(GeniusEntity): async def async_update(self) -> None: """Update an entity's state data.""" - if "_state" in self._device.data: # only for v3 API + if "_state" in self._device.data: # only via v3 API self._last_comms = dt_util.utc_from_timestamp( self._device.data["_state"]["lastComms"] ) @@ -215,6 +274,32 @@ class GeniusZone(GeniusEntity): self._zone = zone self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" + async def _refresh(self, payload: Optional[dict] = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + + if payload["unique_id"] != self._unique_id: + return + + if payload["service"] == SVC_SET_ZONE_OVERRIDE: + temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 + duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) + + await self._zone.set_override(temperature, int(duration.total_seconds())) + return + + mode = payload["data"][ATTR_ZONE_MODE] + + # pylint: disable=protected-access + if mode == "footprint" and not self._zone._has_pir: + raise TypeError( + f"'{self.entity_id}' can not support footprint mode (it has no PIR)" + ) + + await self._zone.set_mode(mode) + @property def name(self) -> str: """Return the name of the climate device.""" diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml new file mode 100644 index 00000000000..d7522ac2995 --- /dev/null +++ b/homeassistant/components/geniushub/services.yaml @@ -0,0 +1,29 @@ +# Support for a Genius Hub system +# Describes the format for available services + +set_zone_mode: + description: >- + Set the zone to an operating mode. + fields: + entity_id: + description: The zone's entity_id. + example: climate.kitchen + mode: + description: 'One of: off, timer or footprint.' + example: timer + +set_zone_override: + description: >- + Override the zone's setpoint for a given duration. + fields: + entity_id: + description: The zone's entity_id. + example: climate.bathroom + temperature: + description: The target temperature, to 0.1 C. + example: 19.2 + duration: + description: >- + The duration of the override. Optional, default 1 hour, maximum 24 hours. + example: '{"minutes": 135}' +