diff --git a/custom_components/dirigera_platform/__init__.py b/custom_components/dirigera_platform/__init__.py index 9abcc27..6e54745 100644 --- a/custom_components/dirigera_platform/__init__.py +++ b/custom_components/dirigera_platform/__init__.py @@ -5,12 +5,13 @@ import logging from dirigera import Hub +from dirigera.devices.scene import Scene as DirigeraScene import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components.light import PLATFORM_SCHEMA -from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN +from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN, CONF_ENTITY_ID, CONF_TYPE # Import the device class from the component that you want to support from homeassistant.core import HomeAssistant @@ -34,9 +35,10 @@ hub_events = None async def async_setup(hass: HomeAssistant, config: dict) -> bool: - logger.debug("Starting async_setup...") - logger.debug(config) - logger.debug("Complete async_setup...") + logger.error("Starting async_setup...") + for k in config.keys(): + logger.error(f"config key: {k} value: {config[k]}") + logger.error("Complete async_setup...") def handle_dump_data(call): import dirigera @@ -68,9 +70,9 @@ async def async_setup_entry( ) -> bool: global hub_events """Set up platform from a ConfigEntry.""" - logger.debug("Staring async_setup_entry in init...") - logger.debug(dict(entry.data)) - + logger.error("Staring async_setup_entry in init...") + logger.error(dict(entry.data)) + logger.error(f"async_setup_entry {entry.unique_id} {entry.state} {entry.entry_id} {entry.title} {entry.domain}") hass.data.setdefault(DOMAIN, {}) hass_data = dict(entry.data) @@ -106,7 +108,7 @@ async def async_setup_entry( hub = Hub(hass_data[CONF_TOKEN], hass_data[CONF_IP_ADDRESS]) if hass_data[CONF_IP_ADDRESS] != "mock": - hub_events = hub_event_listener(hub) + hub_events = hub_event_listener(hub, hass) hub_events.start() logger.debug("Complete async_setup_entry...") @@ -121,7 +123,6 @@ async def options_update_listener( """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) - async def async_unload_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: @@ -134,6 +135,20 @@ async def async_unload_entry( hub_events.stop() hub_events = None + hass_data = dict(entry.data) + hub = Hub(hass_data[CONF_TOKEN], hass_data[CONF_IP_ADDRESS]) + + # For each controller if there is an empty scene delete it + logger.error("In unload so forcing delete of scenes...") + scenes: list[DirigeraScene] = await hass.async_add_executor_job(hub.get_scenes) + for scene in scenes: + if scene.info.name is None or not scene.info.name.startswith("dirigera_platform_empty_scene_"): + logger.error(f"Ignoring scene : {scene.info.name}, as not empty scene") + continue + logger.error(f"Deleting scene {scene.id}...") + await hass.async_add_executor_job(hass.delete_scene,scene.id) + logger.error("Done deleting scene....") + """Unload a config entry.""" unload_ok = all( [ diff --git a/custom_components/dirigera_platform/dirigera_lib_patch.py b/custom_components/dirigera_platform/dirigera_lib_patch.py index aaa88ed..00fdc2c 100644 --- a/custom_components/dirigera_platform/dirigera_lib_patch.py +++ b/custom_components/dirigera_platform/dirigera_lib_patch.py @@ -22,6 +22,20 @@ def get_controllers(self) -> List[ControllerX]: devices = self.get("/devices") controllers = list(filter(lambda x: x["type"] == "controller", devices)) return [dict_to_controller(controller, self) for controller in controllers] + + def create_empty_scene(self, name:str): + data = { + "info": { "name" : name , "icon" : "scenes_trophy"}, + "type": "customScene", + "triggers": [], + "actions": [] + } + + #data = camelize_dict(data) # type: ignore + response_dict = self.post( + "/scenes", + data=data, + ) class ControllerAttributesX(Attributes): is_on: Optional[bool] = None diff --git a/custom_components/dirigera_platform/hub_event_listener.py b/custom_components/dirigera_platform/hub_event_listener.py index 370fcef..757f4b9 100644 --- a/custom_components/dirigera_platform/hub_event_listener.py +++ b/custom_components/dirigera_platform/hub_event_listener.py @@ -5,9 +5,13 @@ import re import websocket import ssl +import re from typing import Any from dirigera import Hub +from .const import DOMAIN + +from homeassistant.const import ATTR_ENTITY_ID logger = logging.getLogger("custom_components.dirigera_platform") @@ -60,14 +64,96 @@ def get_registry_entry(id:str) -> registry_entry: return None return hub_event_listener.device_registry[id] - def __init__(self, hub : Hub): + def __init__(self, hub : Hub, hass): super().__init__() self._hub : Hub = hub self._request_to_stop = False + self._hass = hass def on_error(self, ws:Any, ws_msg:str): logger.debug(f"on_error hub event listener {ws_msg}") + def parse_scene_update(self, msg): + # Verify that this is controller initiated + if "data" not in msg: + logger.error(f"discarding message as key 'data' not found: {msg}") + return + + if "triggers" not in msg["data"]: + logger.error(f"discarding message as key 'data/triggers'") + return + + triggers = msg["data"]["triggers"] + + for trigger in triggers: + if "type" not in trigger: + logger.error(f"key 'type' not in trigger json : {trigger}") + continue + + if trigger["type"] != "controller": + continue + + if "trigger" not in trigger: + logger.error(f"key 'trigger' not found in trigger json: {trigger}") + continue + + details = trigger["trigger"] + + if "controllerType" not in details or "clickPattern" not in details or "deviceId" not in details: + logger.error(f"Required key controllerType/clickPattern/deviceId not in trigger json : {trigger}") + continue + + controller_type = details["controllerType"] + click_pattern = details["clickPattern"] + device_id = details["deviceId"] + + if controller_type != "shortcutController": + logger.error(f"controller type on message not compatible {controller_type}, ignoring...") + continue + + if click_pattern == "singlePress": + trigger_type = "single_click" + elif click_pattern == "longPress": + trigger_type = "long_press" + elif click_pattern == "double_click": + trigger_type == "double_click" + else: + logger.error(f"click_pattern : {click_pattern} not in list of types...ignoring") + continue + + device_id_for_registry = device_id + + button_idx = 0 + pattern = '(([0-9]|[a-z]|-)*)_([0-9])+' + match = re.search(pattern, device_id) + if match is not None: + device_id_for_registry = f"{match.groups()[0]}_1" + button_idx = int(match.groups()[2]) + logger.error(f"Multi button controller, device_id effective : {device_id_for_registry} with buttons : {button_idx}") + + if button_idx != 0: + trigger_type =f"button{button_idx}_{trigger_type}" + + # Now look up the associated entity in our own registry + registry_value = hub_event_listener.get_registry_entry(device_id_for_registry) + + if registry_value.__class__.__name__ != "registry_entry": + logger.error(f"id : {device_id_for_registry} listener registry is not correct : {registry_value.__class__.__name__}...") + continue + + entity = registry_value.entity + + # Now raise the bus event + event_data = { + "type": trigger_type, + "device_id": entity.registry_entry.device_id, + ATTR_ENTITY_ID: entity.registry_entry.entity_id + } + + self._hass.bus.async_fire(event_type="dirigera_platform_event",event_data=event_data) + logger.error(f"Event fired.. {event_data}") + logger.error(f"{self.registry_entry}") + def on_message(self, ws:Any, ws_msg:str): try: diff --git a/custom_components/dirigera_platform/scene.py b/custom_components/dirigera_platform/scene.py index c86aa64..a6ef295 100644 --- a/custom_components/dirigera_platform/scene.py +++ b/custom_components/dirigera_platform/scene.py @@ -24,6 +24,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Create scene entities from Dirigera scene.""" + logger.debug("async_setup_entry for scenes...") config = hass.data[DOMAIN][entry.entry_id] hub = Hub(config[CONF_TOKEN], config[CONF_IP_ADDRESS]) @@ -35,8 +36,9 @@ async def async_setup_entry( return scenes: list[DirigeraScene] = await hass.async_add_executor_job(hub.get_scenes) + logger.error(f"Found {len(scenes)} scenes...") entities: list[IkeaScene] = [IkeaScene(hub, s) for s in scenes] - logger.debug("Found %d scenes", len(entities)) + logger.error("Found %d scenes", len(entities)) async_add_entities(entities) @@ -78,4 +80,4 @@ async def async_update(self) -> None: "Error encountered on update of '%s' (%s)", self.name, self.unique_id ) logger.error(ex) - raise HomeAssistantError from ex + raise HomeAssistantError from ex \ No newline at end of file diff --git a/custom_components/dirigera_platform/sensor.py b/custom_components/dirigera_platform/sensor.py index aa4d083..9956969 100644 --- a/custom_components/dirigera_platform/sensor.py +++ b/custom_components/dirigera_platform/sensor.py @@ -6,6 +6,7 @@ from .dirigera_lib_patch import HubX from dirigera.devices.environment_sensor import EnvironmentSensor from dirigera.devices.controller import Controller +from dirigera.devices.scene import Info, Icon from homeassistant.helpers.entity import Entity from homeassistant import config_entries, core @@ -25,6 +26,10 @@ async def async_setup_entry( ): logger.debug("EnvSensor & Controllers Starting async_setup_entry") """Setup sensors from a config entry created in the integrations UI.""" + logger.error("Staring async_setup_entry in SENSOR...") + logger.error(dict(config_entry.data)) + logger.error(f"async_setup_entry SENSOR {config_entry.unique_id} {config_entry.state} {config_entry.entry_id} {config_entry.title} {config_entry.domain}") + config = hass.data[DOMAIN][config_entry.entry_id] logger.debug(config) @@ -57,6 +62,7 @@ async def async_setup_entry( ] hub_controllers = await hass.async_add_executor_job(hub.get_controllers) + logger.error(f"Got {len(hub_controllers)} controllers...") controller_devices = [ ikea_controller(hass, hub, controller_device) for controller_device in hub_controllers @@ -64,6 +70,17 @@ async def async_setup_entry( # This is not the case of the second device for SOMRIG controllers if controller_device.attributes.battery_percentage ] + + for controller in controller_devices: + # Hack to create empty scene so that we can associate it the controller + # so that click of buttons on the controller can generate events on the hub + #hub.create(name=f"dirigera_platform_empty_scene_{controller.unique_id}",icon="scenes_heart") + scene_name=f"dirigera_platform_empty_scene_{controller.unique_id}" + #scene_info.name = f"dirigera_platform_empty_scene_{controller.unique_id}" + #scene_info.icon = "scenes_heart" + logger.error(f"Creating empty scene {scene_name} for controller...") + await hass.async_add_executor_job(hub.create_empty_scene,scene_name) + env_sensors = [] for env_device in env_devices: @@ -201,9 +218,22 @@ def native_value(self) -> int: def native_unit_of_measurement(self) -> str: return "µg/m³" +# SOMRIG Controllers act differently in the gateway Hub +# While its one device but two id's are sent back each +# representing the two buttons on the controler. The id is +# all same except _1 and _2 suffix. The serial number on the +# controllers is same. + +CONTROLLER_BUTTON_MAP = { "SOMRIG shortcut button" : 2 } + class ikea_controller(ikea_base_device, SensorEntity): def __init__(self,hass:core.HomeAssistant, hub:Hub, json_data:Controller): logger.debug("ikea_controller ctor...") + self._buttons = 1 + if json_data.attributes.model in CONTROLLER_BUTTON_MAP: + self._buttons = CONTROLLER_BUTTON_MAP[json_data.attributes.model] + logger.error(f"Set #buttons to {self._buttons} as controller model is : {json_data.attributes.model}") + super().__init__(hass , hub, json_data, hub.get_controller_by_id) @property @@ -225,9 +255,10 @@ def native_unit_of_measurement(self) -> str: @property def device_class(self) -> str: return SensorDeviceClass.BATTERY + + @property + def number_of_buttons(self) -> int: + return self._buttons -# SOMRIG Controllers act differently in the gateway Hub -# While its one device but two id's are sent back each -# representing the two buttons on the controler. The id is -# all same except _1 and _2 suffix. The serial number on the -# controllers is same. \ No newline at end of file + async def async_update(self): + pass \ No newline at end of file diff --git a/custom_components/dirigera_platform/strings.json b/custom_components/dirigera_platform/strings.json index 453c655..c24713f 100644 --- a/custom_components/dirigera_platform/strings.json +++ b/custom_components/dirigera_platform/strings.json @@ -28,5 +28,18 @@ "description": "Update IKEA Dirigera Hub Setting..." } } - } + }, + "device_automation": { + "trigger_type": { + "single_click": "{entity_name} Single Click", + "long_press": "{entity_name} Long Press", + "double_click": "{entity_name} Double Click", + "button1_single_click": "{entity_name} Button 1 Single Click", + "button1_long_press": "{entity_name} Button 1 Long Press", + "button1_double_click": "{entity_name} Button 1 Double Click", + "button2_single_click": "{entity_name} Button 2 Single Click", + "button2_long_press": "{entity_name} Button 2 Long Press", + "button2_double_click": "{entity_name} Button 2 Double Click" + } + } } \ No newline at end of file diff --git a/custom_components/dirigera_platform/translations/en.json b/custom_components/dirigera_platform/translations/en.json index 9f5ccf4..0679a06 100644 --- a/custom_components/dirigera_platform/translations/en.json +++ b/custom_components/dirigera_platform/translations/en.json @@ -50,5 +50,18 @@ "hub_exception": { "message" : "Exception raised by Hub..." } - } + }, + "device_automation": { + "trigger_type": { + "single_click": "{entity_name} Single Click", + "long_press": "{entity_name} Long Press", + "double_click": "{entity_name} Double Click", + "button1_single_click": "{entity_name} Button 1 Single Click", + "button1_long_press": "{entity_name} Button 1 Long Press", + "button1_double_click": "{entity_name} Button 1 Double Click", + "button2_single_click": "{entity_name} Button 2 Single Click", + "button2_long_press": "{entity_name} Button 2 Long Press", + "button2_double_click": "{entity_name} Button 2 Double Click" + } + } } \ No newline at end of file