From dece26ef3d2a73e15f147bf55a80dff7cf5df927 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Sat, 16 Mar 2024 01:19:20 +0100 Subject: [PATCH] add home assistant integration --- .../home_assistant_integration/__init__.py | 55 ++++++++++++ .../home_assistant_integration/config_flow.py | 29 +++++++ vendors/home_assistant_integration/const.py | 3 + vendors/home_assistant_integration/hub.py | 48 +++++++++++ .../home_assistant_integration/manifest.json | 14 +++ vendors/home_assistant_integration/sensor.py | 59 +++++++++++++ .../home_assistant_integration/strings.json | 13 +++ .../websocket_api.py | 86 +++++++++++++++++++ 8 files changed, 307 insertions(+) create mode 100644 vendors/home_assistant_integration/__init__.py create mode 100644 vendors/home_assistant_integration/config_flow.py create mode 100644 vendors/home_assistant_integration/const.py create mode 100644 vendors/home_assistant_integration/hub.py create mode 100644 vendors/home_assistant_integration/manifest.json create mode 100644 vendors/home_assistant_integration/sensor.py create mode 100644 vendors/home_assistant_integration/strings.json create mode 100644 vendors/home_assistant_integration/websocket_api.py diff --git a/vendors/home_assistant_integration/__init__.py b/vendors/home_assistant_integration/__init__.py new file mode 100644 index 0000000..c9447f1 --- /dev/null +++ b/vendors/home_assistant_integration/__init__.py @@ -0,0 +1,55 @@ +"""The Immersive Home integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from . import hub, websocket_api +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the ImmersiveHome component.""" + + hass.data[DOMAIN] = { + "hub": hub.Hub(hass), + } + + websocket_api.async_setup_commands(hass) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up ImmersiveHome from a config entry.""" + registration = entry.data + + hass.data.setdefault(DOMAIN, {}) + + device_registry = dr.async_get(hass) + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, registration["device_id"])}, + manufacturer="Someone", + model=registration["platform"], + name=registration["name"], + sw_version=registration["version"], + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/vendors/home_assistant_integration/config_flow.py b/vendors/home_assistant_integration/config_flow.py new file mode 100644 index 0000000..783fc63 --- /dev/null +++ b/vendors/home_assistant_integration/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Immersive Home.""" + +from homeassistant.config_entries import ConfigFlow + +from .const import DOMAIN + + +class ImmersiveHomeFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a Mobile App config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + placeholders = { + "apps_url": "https://www.home-assistant.io/integrations/immersive_home/#apps" + } + + return self.async_abort( + reason="install_app", description_placeholders=placeholders + ) + + async def async_step_registration(self, user_input=None): + """Handle a flow initialized during registration.""" + + if "device_id" in user_input: + await self.async_set_unique_id(f"{user_input['device_id']}") + + return self.async_create_entry(title=user_input["name"], data=user_input) diff --git a/vendors/home_assistant_integration/const.py b/vendors/home_assistant_integration/const.py new file mode 100644 index 0000000..dff5369 --- /dev/null +++ b/vendors/home_assistant_integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the Immersive Home integration.""" + +DOMAIN = "immersive_home" diff --git a/vendors/home_assistant_integration/hub.py b/vendors/home_assistant_integration/hub.py new file mode 100644 index 0000000..32f094a --- /dev/null +++ b/vendors/home_assistant_integration/hub.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import asyncio + +from homeassistant.core import HomeAssistant + + +class Hub: + manufacturer = "Immersive Home" + + def __init__(self, hass: HomeAssistant) -> None: + self._hass = hass + self.devices = {} + + def add_device(self, device: Device) -> None: + self.devices[device.id] = device + + def get_device(self, device_id: str) -> Device: + return self.devices.get(device_id) + + +class Device: + def __init__(self, device_id: str, name: str, version: str, platform: str) -> None: + self.id = device_id + self.name = name + self.version = version + self.platform = platform + self.room = None + + self._callbacks = set() + + def set_room(self, room: str) -> None: + self.room = room + + asyncio.create_task(self.publish_updates()) + + def add_callback(self, callback: callable[[], None]) -> None: + """Register callback, called when Roller changes state.""" + self._callbacks.add(callback) + + def remove_callback(self, callback: callable[[], None]) -> None: + """Remove previously registered callback.""" + self._callbacks.discard(callback) + + async def publish_updates(self) -> None: + """Schedule call all registered callbacks.""" + for callback in self._callbacks: + callback() diff --git a/vendors/home_assistant_integration/manifest.json b/vendors/home_assistant_integration/manifest.json new file mode 100644 index 0000000..7495f74 --- /dev/null +++ b/vendors/home_assistant_integration/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "immersive_home", + "name": "ImmersiveHome", + "codeowners": ["@nitwel"], + "config_flow": true, + "dependencies": ["http", "websocket_api"], + "documentation": "https://www.home-assistant.io/integrations/immersive_home", + "homekit": {}, + "iot_class": "local_push", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "version": "0.0.1" +} diff --git a/vendors/home_assistant_integration/sensor.py b/vendors/home_assistant_integration/sensor.py new file mode 100644 index 0000000..0467324 --- /dev/null +++ b/vendors/home_assistant_integration/sensor.py @@ -0,0 +1,59 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import DOMAIN +from .hub import Device, Hub + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the sensor platform.""" + + hub: Hub = hass.data[DOMAIN]["hub"] + + for device in hub.devices.values(): + async_add_entities([RoomSensor(device)]) + + +class RoomSensor(SensorEntity): + """Representation of a Sensor.""" + + def __init__(self, device: Device) -> None: + """Initialize the sensor.""" + self._attr_name = f"{device.name} Room" + self._attr_unique_id = f"{device.id}_room" + self._device = device + self._attr_should_poll = False + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self._device.id)}, + } + + @property + def state(self): + return self._device.room + + @property + def available(self): + return True + + async def async_added_to_hass(self): + """Run when this Entity has been added to HA.""" + # Sensors should also register callbacks to HA when their state changes + self._device.add_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + # The opposite of async_added_to_hass. Remove any registered call backs here. + self._device.remove_callback(self.async_write_ha_state) diff --git a/vendors/home_assistant_integration/strings.json b/vendors/home_assistant_integration/strings.json new file mode 100644 index 0000000..ad8f0f4 --- /dev/null +++ b/vendors/home_assistant_integration/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/vendors/home_assistant_integration/websocket_api.py b/vendors/home_assistant_integration/websocket_api.py new file mode 100644 index 0000000..78e7143 --- /dev/null +++ b/vendors/home_assistant_integration/websocket_api.py @@ -0,0 +1,86 @@ +"""Home Assistant websocket API.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .hub import Device, Hub + + +@callback +def async_setup_commands(hass): + """Set up the mobile app websocket API.""" + websocket_api.async_register_command(hass, handle_register) + websocket_api.async_register_command(hass, handle_update) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "immersive_home/register", + vol.Required("device_id"): str, + vol.Required("name"): str, + vol.Required("version"): str, + vol.Required("platform"): str, + } +) +@websocket_api.async_response +async def handle_register( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Set up a new Immersive Home Device.""" + + hub: Hub = hass.data[DOMAIN]["hub"] + + hub.add_device( + Device( + msg["device_id"], + msg["name"], + msg["version"], + msg["platform"], + ) + ) + + await hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, data=msg, context={"source": "registration"} + ) + ) + + connection.send_result(msg["id"], {"result": "success"}) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "immersive_home/update", + vol.Required("device_id"): str, + vol.Optional("room"): str, + } +) +def handle_update( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Update data of a Immersive Home Device.""" + + hub: Hub = hass.data[DOMAIN]["hub"] + device = hub.get_device(msg["device_id"]) + + if device is None: + connection.send_error(msg["id"], "device_not_found", "Device not found") + return + + if "room" in msg: + device.set_room(msg["room"]) + + connection.send_result(msg["id"], {"result": "success"})