From 94daa6d0888c105094ff11e119e43d838507e5e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Feb 2023 21:27:33 +0100 Subject: [PATCH] Add Home Assistant cloud controls --- README.md | 7 ++ custom_components/spook/const.py | 2 +- custom_components/spook/entity.py | 27 ++++++ custom_components/spook/manifest.json | 1 + custom_components/spook/switch.py | 129 ++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 custom_components/spook/switch.py diff --git a/README.md b/README.md index d4b8bb24..2ca323fb 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,13 @@ of the box. - **Number of suns**: Answers how godlike you are. _#burn_ - **Number of zones**: How many comfort zones you have on a map. _#zoneing_ +## Switches + +- **Home Assistant Cloud**: Switch to control behavior of Nabu Casa's Home Assistant Cloud. _#love_ + - **Alexa**: Enable/disable Alexa connection. _#amazon_ + - **Google Assistant**: Enable/disable Google Assistant connection. _#bigtech_ + - **Remote**: Enable/disable remote access to the Home Asistant frontend. _#rdp_ + # Services There are quite a few useless and horrible services available for you to explore diff --git a/custom_components/spook/const.py b/custom_components/spook/const.py index 6dd204f9..79bdd263 100644 --- a/custom_components/spook/const.py +++ b/custom_components/spook/const.py @@ -4,4 +4,4 @@ from homeassistant.const import Platform DOMAIN: Final = "spook" -PLATFORMS: Final = [Platform.SENSOR] +PLATFORMS: Final = [Platform.SENSOR, Platform.SWITCH] diff --git a/custom_components/spook/entity.py b/custom_components/spook/entity.py index 36f7a745..9ea728ff 100644 --- a/custom_components/spook/entity.py +++ b/custom_components/spook/entity.py @@ -3,7 +3,9 @@ from dataclasses import dataclass +from hass_nabucasa import Cloud from homeassistant.components import homeassistant +from homeassistant.components.cloud.const import DOMAIN as CLOUD_DOMAIN from homeassistant.const import __version__ as HA_VERSION from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription @@ -44,3 +46,28 @@ def __init__(self, description: SpookEntityDescription) -> None: sw_version=HA_VERSION, ) self._attr_unique_id = f"{homeassistant.DOMAIN}_{description.key}" + + +class HomeAssistantCloudSpookEntity(SpookEntity): + """Defines an base Spook entity for Home Assistant Cloud related entities.""" + + _cloud: Cloud + + def __init__(self, cloud: Cloud, description: SpookEntityDescription) -> None: + """Initialize the entity.""" + super().__init__(description=description) + self._cloud = cloud + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, CLOUD_DOMAIN)}, + manufacturer="Nabu Casa Inc.", + name="Home Assistant Cloud", + configuration_url="https://account.nabucasa.com/", + ) + self._attr_unique_id = f"{CLOUD_DOMAIN}_{description.key}" + + @property + def available(self) -> bool: + """Return if cloud services are available.""" + return ( + super().available and self._cloud.is_logged_in and self._cloud.is_connected + ) diff --git a/custom_components/spook/manifest.json b/custom_components/spook/manifest.json index ab08f2b2..f26f66a5 100644 --- a/custom_components/spook/manifest.json +++ b/custom_components/spook/manifest.json @@ -4,6 +4,7 @@ "codeowners": ["@frenck"], "config_flow": true, "dependencies": ["homeassistant"], + "after_dependencies": ["cloud"], "documentation": "https://github.com/frenck/spook", "integration_type": "hub", "iot_class": "local_push", diff --git a/custom_components/spook/switch.py b/custom_components/spook/switch.py new file mode 100644 index 00000000..3e841575 --- /dev/null +++ b/custom_components/spook/switch.py @@ -0,0 +1,129 @@ +"""Spook - Not your homey.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from hass_nabucasa import Cloud +from homeassistant.components.cloud.const import DOMAIN as CLOUD_DOMAIN +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import HomeAssistantCloudSpookEntity, SpookEntityDescription + + +@dataclass +class HomeAssistantCloudSpookSwitchEntityDescriptionMixin: + """Mixin values for Home Assistant related sensors.""" + + is_on_fn: Callable[[Cloud], bool | None] + set_fn: Callable[[Cloud, bool], Awaitable[Any]] + + +@dataclass +class HomeAssistantCloudSpookSwitchEntityDescription( + SpookEntityDescription, + SwitchEntityDescription, + HomeAssistantCloudSpookSwitchEntityDescriptionMixin, +): + """Class describing Spook Home Assistant sensor entities.""" + + icon_off: str | None = None + + def __post_init__(self) -> None: + """Sync icon_off with icon.""" + if self.icon_off is None: + self.icon_off = self.icon + + +SWITCHES: tuple[HomeAssistantCloudSpookSwitchEntityDescription, ...] = ( + HomeAssistantCloudSpookSwitchEntityDescription( + key="alexa", + entity_id="switch.cloud_alexa", + name="Alexa", + icon="mdi:account-voice", + entity_category=EntityCategory.CONFIG, + is_on_fn=lambda cloud: cloud.client.prefs.alexa_enabled, + set_fn=lambda cloud, enabled: cloud.client.prefs.async_update( + alexa_enabled=enabled + ), + ), + HomeAssistantCloudSpookSwitchEntityDescription( + key="google", + entity_id="switch.cloud_google", + name="Google Assistant", + icon="mdi:google-assistant", + entity_category=EntityCategory.CONFIG, + is_on_fn=lambda cloud: cloud.client.prefs.google_enabled, + set_fn=lambda cloud, enabled: cloud.client.prefs.async_update( + google_enabled=enabled + ), + ), + HomeAssistantCloudSpookSwitchEntityDescription( + key="remote", + entity_id="switch.cloud_remote", + name="Remote", + icon="mdi:remote-desktop", + entity_category=EntityCategory.CONFIG, + is_on_fn=lambda cloud: cloud.client.prefs.remote_enabled, + set_fn=lambda cloud, enabled: cloud.client.prefs.async_update( + remote_enabled=enabled + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Spook sensor.""" + if CLOUD_DOMAIN in hass.config.components: + cloud: Cloud = hass.data[CLOUD_DOMAIN] + async_add_entities( + HomeAssistantCloudSpookSwitchEntity(cloud, description) + for description in SWITCHES + ) + + +class HomeAssistantCloudSpookSwitchEntity(HomeAssistantCloudSpookEntity, SwitchEntity): + """Spook switch providig Home Asistant Cloud controls.""" + + entity_description: HomeAssistantCloudSpookSwitchEntityDescription + + async def async_added_to_hass(self) -> None: + """Register for switch updates.""" + + @callback + def _update_state(_: Any) -> None: + """Update state.""" + self.async_schedule_update_ha_state() + + self.async_on_remove( + self._cloud.client.prefs.async_listen_updates(_update_state) + ) + + @property + def icon(self) -> str | None: + """Return the icon.""" + if self.entity_description.icon_off and self.is_on is False: + return self.entity_description.icon_off + return super().icon + + @property + def is_on(self) -> bool | None: + """Return state of the switch.""" + return self.entity_description.is_on_fn(self._cloud) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.set_fn(self._cloud, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.set_fn(self._cloud, False)