diff --git a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/0001-switch-to-internal-socket-client.patch b/buildroot-external/package/python-ovos-phal-plugin-homeassistant/0001-switch-to-internal-socket-client.patch deleted file mode 100644 index f975e3c1..00000000 --- a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/0001-switch-to-internal-socket-client.patch +++ /dev/null @@ -1,582 +0,0 @@ -From 20579c80db5dd50d1b0e53b50a11b9eb2f90deaf Mon Sep 17 00:00:00 2001 -From: Aditya Mehra -Date: Thu, 12 Jan 2023 21:55:33 +1030 -Subject: [PATCH] Switch wsconnector to internal socket client - ---- - ovos_PHAL_plugin_homeassistant/__init__.py | 50 ++-- - .../logic/connector.py | 77 +++-- - .../logic/socketclient.py | 274 ++++++++++++++++++ - requirements.txt | 2 +- - 4 files changed, 336 insertions(+), 67 deletions(-) - create mode 100644 ovos_PHAL_plugin_homeassistant/logic/socketclient.py - -diff --git a/ovos_PHAL_plugin_homeassistant/__init__.py b/ovos_PHAL_plugin_homeassistant/__init__.py -index a6d2daf..7085c14 100644 ---- a/ovos_PHAL_plugin_homeassistant/__init__.py -+++ b/ovos_PHAL_plugin_homeassistant/__init__.py -@@ -32,6 +32,8 @@ def __init__(self, bus=None, config=None): - self.gui = GUIInterface(bus=self.bus, skill_id=self.name) - self.integrator = Integrator(self.bus, self.gui) - self.instance_available = False -+ self.use_ws = False -+ self.enable_debug = self.config.get("enable_debug", False) - self.device_types = { - "sensor": HomeAssistantSensor, - "binary_sensor": HomeAssistantBinarySensor, -@@ -95,14 +97,19 @@ def validate_instance_connection(self, host, api_key): - bool: True if the connection is valid, False otherwise - """ - try: -- if self.config.get('use_websocket'): -- LOG.info("Using websocket connection") -- validator = HomeAssistantWSConnector(host, api_key) -+ if self.use_ws: -+ validator = HomeAssistantWSConnector(host, api_key, self.enable_debug) - else: -- validator = HomeAssistantRESTConnector(host, api_key) -+ validator = HomeAssistantRESTConnector(host, api_key, self.enable_debug) - - validator.get_all_devices() -+ -+ if self.use_ws: -+ if validator.client: -+ validator.disconnect() -+ - return True -+ - except Exception as e: - LOG.error(e) - return False -@@ -118,15 +125,7 @@ def setup_configuration(self, message): - - if host and key: - if host.startswith("ws") or host.startswith("wss"): -- config_patch = { -- "PHAL": { -- "ovos-PHAL-plugin-homeassistant": { -- "use_websocket": True -- } -- } -- } -- update_mycroft_config(config=config_patch, bus=self.bus) -- sleep(2) # wait for config to be updated -+ self.use_ws = True - - if self.validate_instance_connection(host, key): - self.config["host"] = host -@@ -149,21 +148,24 @@ def init_configuration(self, message=None): - """ Initialize instance configuration """ - configuration_host = self.config.get("host", "") - configuration_api_key = self.config.get("api_key", "") -+ if configuration_host.startswith("ws") or configuration_host.startswith("wss"): -+ self.use_ws = True -+ - if not self.config.get("use_group_display"): - self.config["use_group_display"] = False - - if configuration_host != "" and configuration_api_key != "": - self.instance_available = True -- if self.config.get('use_websocket'): -+ if self.use_ws: - self.connector = HomeAssistantWSConnector(configuration_host, -- configuration_api_key) -+ configuration_api_key, self.enable_debug) - else: - self.connector = HomeAssistantRESTConnector( -- configuration_host, configuration_api_key) -+ configuration_host, configuration_api_key, self.enable_debug) - self.devices = self.connector.get_all_devices() - self.registered_devices = [] - self.build_devices() -- self.gui["use_websocket"] = self.config.get("use_websocket", False) -+ self.gui["use_websocket"] = self.use_ws - self.gui["instanceAvailable"] = True - self.bus.emit(Message("ovos.phal.plugin.homeassistant.ready")) - else: -@@ -185,8 +187,10 @@ def build_devices(self): - device_icon = f"mdi:{device_type}" - device_state = device.get("state", None) - device_area = device.get("area_id", None) -- LOG.info( -- f"Device added: {device_name} - {device_type} - {device_area}") -+ if self.enable_debug: -+ LOG.info( -+ f"Device added: {device_name} - {device_type} - {device_area}") -+ - device_attributes = device.get("attributes", {}) - if device_type in self.device_types: - self.registered_devices.append(self.device_types[device_type]( -@@ -396,7 +400,7 @@ def handle_show_dashboard(self, message=None): - message (Message): The message object - """ - if self.instance_available: -- self.gui["use_websocket"] = self.config.get("use_websocket", False) -+ self.gui["use_websocket"] = self.use_ws - if not self.config.get("use_group_display"): - display_list_model = { - "items": self.build_display_dashboard_device_model()} -@@ -420,8 +424,9 @@ def handle_show_dashboard(self, message=None): - self.gui["use_group_display"] = self.config.get("use_group_display", False) - self.gui.show_page(page, override_idle=True) - -- LOG.info("Using group display") -- LOG.info(self.config["use_group_display"]) -+ if self.enable_debug: -+ LOG.debug("Using group display") -+ LOG.debug(self.config["use_group_display"]) - - def handle_close_dashboard(self, message): - """ Handle the close dashboard message -@@ -499,7 +504,6 @@ def handle_set_group_display_settings(self, message): - "ovos-PHAL-plugin-homeassistant": { - "host": self.config.get("host"), - "api_key": self.config.get("api_key"), -- "use_websocket": self.config.get("use_websocket", False), - "use_group_display": use_group_display - } - } -diff --git a/ovos_PHAL_plugin_homeassistant/logic/connector.py b/ovos_PHAL_plugin_homeassistant/logic/connector.py -index 6368444..2cf970c 100644 ---- a/ovos_PHAL_plugin_homeassistant/logic/connector.py -+++ b/ovos_PHAL_plugin_homeassistant/logic/connector.py -@@ -4,20 +4,19 @@ - import requests - import json - import sys --import asyncio - --from hass_client.client import HomeAssistantClient - from ovos_utils.log import LOG -- -+from ovos_PHAL_plugin_homeassistant.logic.socketclient import HomeAssistantClient - - class HomeAssistantConnector: -- def __init__(self, host, api_key): -+ def __init__(self, host, api_key, enable_debug=False): - """ Constructor - - Args: - host (str): The host of the home assistant instance. - api_key (str): The api key - """ -+ self.enable_debug = enable_debug - self.host = host - self.api_key = api_key - -@@ -115,8 +114,9 @@ def call_function(self, device_id, device_type, function, arguments=None): - - - class HomeAssistantRESTConnector(HomeAssistantConnector): -- def __init__(self, host, api_key): -+ def __init__(self, host, api_key, enable_debug=False): - super().__init__(host, api_key) -+ self.enable_debug = enable_debug - self.headers = {'Authorization': 'Bearer ' + - self.api_key, 'content-type': 'application/json'} - -@@ -251,37 +251,33 @@ def call_function(self, device_id, device_type, function, arguments=None): - - - class HomeAssistantWSConnector(HomeAssistantConnector): -- def __init__(self, host, api_key): -+ def __init__(self, host, api_key, enable_debug=False): - super().__init__(host, api_key) - if self.host.startswith('http'): - self.host.replace('http', 'ws', 1) -- try: -- self._loop = asyncio.get_event_loop() -- except RuntimeError: -- asyncio.set_event_loop(asyncio.new_event_loop()) -- self._loop = asyncio.get_event_loop() -- self._client: HomeAssistantClient = None -- self._loop.run_until_complete(self.start_client()) -- -- @property -- def client(self): -- if not self._client.connected: -- LOG.error("Client not connected, re-initializing") -- self._loop.run_until_complete(self.start_client()) -- return self._client -- -- async def start_client(self): -- self._client = self._client or HomeAssistantClient(self.host, -- self.api_key) -- await self._client.connect() -+ self.enable_debug = enable_debug -+ self._connection = HomeAssistantClient(self.host, self.api_key) -+ self._connection.connect() -+ -+ # Initialize client instance -+ self.client = self._connection.get_instance_sync() -+ self.client.build_registries_sync() -+ self.client.register_event_listener(self.event_listener) -+ self.client.subscribe_events_sync() -+ -+ def event_listener(self, message): -+ # Todo: Implementation with UI -+ # For now it uses the old states update method -+ pass - - @staticmethod -- def _device_entry_compat(devices: dict): -+ def _device_entry_compat(devices: dict, enable_debug): - disabled_devices = list() - for idx, dev in devices.items(): - if dev.get('disabled_by'): -- LOG.debug(f'Ignoring {dev.get("entity_id")} disabled by ' -- f'{dev.get("disabled_by")}') -+ if(enable_debug): -+ LOG.debug(f'Ignoring {dev.get("entity_id")} disabled by ' -+ f'{dev.get("disabled_by")}') - disabled_devices.append(idx) - else: - devices[idx].setdefault( -@@ -291,14 +287,12 @@ def _device_entry_compat(devices: dict): - - def get_all_devices(self) -> list: - devices = self.client.entity_registry -- self._device_entry_compat(devices) -+ self._device_entry_compat(devices, self.enable_debug) - devices_with_area = self.assign_group_for_devices(devices) - return list(devices_with_area.values()) - - def get_device_state(self, entity_id: str) -> dict: -- message = {'type': 'get_states'} -- states = self._loop.run_until_complete( -- self.client.send_command(message)) -+ states = self.client.get_states_sync() - for state in states: - if state['entity_id'] == entity_id: - return state -@@ -324,30 +318,27 @@ def get_all_devices_with_type_and_attribute_not_in(self, device_type, attribute, - - def turn_on(self, device_id, device_type): - LOG.debug(f"Turn on {device_id}") -- self._loop.run_until_complete( -- self.client.call_service(device_type, 'turn_on', -- {'entity_id': device_id})) -+ self.client.call_service_sync(device_type, 'turn_on', {'entity_id': device_id}) - - def turn_off(self, device_id, device_type): - LOG.debug(f"Turn off {device_id}") -- self._loop.run_until_complete( -- self.client.call_service(device_type, 'turn_off', -- {'entity_id': device_id})) -+ self.client.call_service_sync(device_type, 'turn_off', {'entity_id': device_id}) - - def call_function(self, device_id, device_type, function, arguments=None): - arguments = arguments or dict() - arguments['entity_id'] = device_id -- self._loop.run_until_complete( -- self.client.call_service(device_type, function, arguments)) -+ self.client.call_service_sync(device_type, function, arguments) - - def assign_group_for_devices(self, devices): -- devices_from_registry = self._loop.run_until_complete( -- self.client.send_command({'type': 'config/device_registry/list'})) -+ devices_from_registry = self.client.send_command_sync('config/device_registry/list') - -- for device_item in devices_from_registry: -+ for device_item in devices_from_registry["result"]: - for device in devices.values(): - if device['device_id'] == device_item['id']: - device['area_id'] = device_item['area_id'] - break - - return devices -+ -+ def disconnect(self): -+ self._connection.disconnect() -\ No newline at end of file -diff --git a/ovos_PHAL_plugin_homeassistant/logic/socketclient.py b/ovos_PHAL_plugin_homeassistant/logic/socketclient.py -new file mode 100644 -index 0000000..abf9d42 ---- /dev/null -+++ b/ovos_PHAL_plugin_homeassistant/logic/socketclient.py -@@ -0,0 +1,274 @@ -+import asyncio -+import json -+import threading -+ -+import requests -+import websockets -+from ovos_utils.log import LOG -+ -+ -+class HomeAssistantClient: -+ def __init__(self, url, token): -+ self.url = url -+ self.token = token -+ self.websocket = None -+ self.loop = asyncio.new_event_loop() -+ asyncio.set_event_loop(self.loop) -+ self.response_queue = asyncio.Queue() -+ self.event_queue = asyncio.Queue() -+ self.id_list = [] -+ self.thread = threading.Thread(target=self.run) -+ self.last_id = None -+ self.event_listener = None -+ self.authenticated = False -+ -+ self._device_registry = {} -+ self._entity_registry = {} -+ self._area_registry = {} -+ -+ async def authenticate(self): -+ await self.websocket.send(f'{{"type": "auth", "access_token": "{self.token}"}}') -+ message = await self.websocket.recv() -+ message = json.loads(message) -+ if message.get("type") == "auth_ok": -+ self.authenticated = True -+ await self.listen() -+ else: -+ self.authenticated = False -+ LOG.error("WS HA Connection Failed to authenticate") -+ return -+ -+ async def _connect(self): -+ try: -+ uri = f"{self.url}/api/websocket" -+ self.websocket = await websockets.connect(uri) -+ -+ # Wait for the auth_required message -+ message = await self.websocket.recv() -+ message = json.loads(message) -+ if message.get("type") == "auth_required": -+ await self.authenticate() -+ if not self.authenticated: -+ return -+ else: -+ raise Exception("Expected auth_required message") -+ except Exception as e: -+ LOG.error(e) -+ await self._disconnect() -+ return -+ -+ async def _disconnect(self): -+ if self.websocket is not None: -+ await self.websocket.close() -+ self.websocket = None -+ -+ async def listen(self): -+ while self.websocket is not None: -+ message = await self.websocket.recv() -+ message = json.loads(message) -+ if message.get("type") == "event": -+ if self.event_listener is not None: -+ self.event_listener(message) -+ else: -+ await self.event_queue.put(message) -+ else: -+ await self.response_queue.put(message) -+ -+ async def send_command(self, command): -+ id = self.counter -+ self.last_id = id -+ message = { -+ "id": id, -+ "type": command -+ } -+ await self.websocket.send(json.dumps(message)) -+ -+ async def send_raw_command(self, command): -+ id = self.counter -+ self.last_id = id -+ message = { -+ "id": id, -+ "type": command -+ } -+ await self.websocket.send(json.dumps(message)) -+ response = await self.response_queue.get() -+ self.response_queue.task_done() -+ return response -+ -+ async def call_service(self, domain, service, service_data): -+ id = self.counter -+ self.last_id = id -+ message = { -+ "id": id, -+ "type": "call_service", -+ "domain": domain, -+ "service": service, -+ "service_data": service_data -+ } -+ await self.websocket.send(json.dumps(message)) -+ response = await self.response_queue.get() -+ self.response_queue.task_done() -+ return response -+ -+ async def get_states(self): -+ await self.send_command("get_states") -+ message = await self.response_queue.get() -+ self.response_queue.task_done() -+ if message.get("result") is None: -+ LOG.info("No states found") -+ return [] -+ else: -+ return message["result"] -+ -+ async def subscribe_events(self): -+ await self.send_command("subscribe_events") -+ message = await self.response_queue.get() -+ self.response_queue.task_done() -+ return message -+ -+ async def get_instance(self): -+ while self.websocket is None: -+ await asyncio.sleep(0.1) -+ return self -+ -+ async def build_registries(self): -+ # First clean the registries -+ self._device_registry = {} -+ self._entity_registry = {} -+ self._area_registry = {} -+ -+ # device registry -+ await self.send_command("config/device_registry/list") -+ message = await self.response_queue.get() -+ self.response_queue.task_done() -+ for item in message["result"]: -+ item_id = item["id"] -+ self._device_registry[item_id] = item -+ -+ # entity registry -+ await self.send_command("config/entity_registry/list") -+ message = await self.response_queue.get() -+ self.response_queue.task_done() -+ for item in message["result"]: -+ item_id = item["entity_id"] -+ self._entity_registry[item_id] = item -+ -+ # area registry -+ await self.send_command("config/area_registry/list") -+ message = await self.response_queue.get() -+ self.response_queue.task_done() -+ for item in message["result"]: -+ item_id = item["area_id"] -+ self._area_registry[item_id] = item -+ -+ return True -+ -+ @property -+ def device_registry(self) -> dict: -+ """Return device registry.""" -+ if not self._device_registry: -+ asyncio.run_coroutine_threadsafe( -+ self.build_registries(), self.loop) -+ LOG.debug("Registry is empty, building registry first.") -+ return self._device_registry -+ -+ @property -+ def entity_registry(self) -> dict: -+ """Return device registry.""" -+ if not self._entity_registry: -+ asyncio.run_coroutine_threadsafe( -+ self.build_registries(), self.loop) -+ LOG.debug("Registry is empty, building registry first.") -+ return self._entity_registry -+ -+ @property -+ def area_registry(self) -> dict: -+ """Return device registry.""" -+ if not self._area_registry: -+ asyncio.run_coroutine_threadsafe( -+ self.build_registries(), self.loop) -+ LOG.debug("Registry is empty, building registry first.") -+ return self._area_registry -+ -+ @property -+ def counter(self): -+ if len(self.id_list) == 0: -+ self.id_list.append(1) -+ return 1 -+ else: -+ new_id = max(self.id_list) + 1 -+ self.id_list.append(new_id) -+ return new_id -+ -+ def set_state(self, entity_id, state, attributes): -+ id = self.counter -+ self.last_id = id -+ data = { -+ "id": id, -+ "type": "get_state", -+ "entity_id": entity_id, -+ "state": state, -+ "attributes": attributes -+ } -+ response = self._post_request(f"states/{entity_id}", data) -+ return response -+ -+ def _post_request(self, endpoint, data): -+ url = self.url.replace("wss", "https").replace("ws", "http") -+ full_url = f"{url}{endpoint}" -+ headers = { -+ "Authorization": f"Bearer {self.token}", -+ "Content-Type": "application/json", -+ } -+ response = requests.post(full_url, headers=headers, json=data) -+ return response.json() -+ -+ def run(self): -+ self.loop.run_until_complete(self._connect()) -+ LOG.info(self.loop.is_running()) -+ -+ def connect(self): -+ self.thread.start() -+ -+ def disconnect(self): -+ asyncio.run_coroutine_threadsafe(self._disconnect(), self.loop) -+ self.thread.join() -+ -+ def get_states_sync(self): -+ task = asyncio.run_coroutine_threadsafe(self.get_states(), self.loop) -+ return task.result() -+ -+ def subscribe_events_sync(self): -+ task = asyncio.run_coroutine_threadsafe( -+ self.subscribe_events(), self.loop) -+ return task.result() -+ -+ def get_instance_sync(self): -+ task = asyncio.run_coroutine_threadsafe(self.get_instance(), self.loop) -+ return task.result() -+ -+ def get_event_sync(self): -+ task = asyncio.run_coroutine_threadsafe( -+ self.event_queue.get(), self.loop) -+ return task.result() -+ -+ def build_registries_sync(self): -+ task = asyncio.run_coroutine_threadsafe( -+ self.build_registries(), self.loop) -+ return task.result() -+ -+ def register_event_listener(self, listener): -+ self.event_listener = listener -+ -+ def unregister_event_listener(self): -+ self.event_listener = None -+ -+ def send_command_sync(self, command): -+ task = asyncio.run_coroutine_threadsafe( -+ self.send_raw_command(command), self.loop) -+ return task.result() -+ -+ def call_service_sync(self, domain, service, service_data): -+ task = asyncio.run_coroutine_threadsafe( -+ self.call_service(domain, service, service_data), self.loop) -+ return task.result() -diff --git a/requirements.txt b/requirements.txt -index 690bf0b..669fcbf 100644 ---- a/requirements.txt -+++ b/requirements.txt -@@ -4,4 +4,4 @@ ovos-config~=0.0.5 - mycroft-messagebus-client - youtube-search - pytube --hass-client~=0.1 -\ No newline at end of file -+websockets diff --git a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.hash b/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.hash index 4528c599..b5e6acaa 100644 --- a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.hash +++ b/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.hash @@ -1 +1 @@ -sha256 8e012f0772f103d94a3ba45105f9063bac9d2af3ca6b27eedb251ed25855d1d7 python-ovos-phal-plugin-homeassistant-5f07ec0053685c197e3da7dca617682a7e1e7ea5.tar.gz +sha256 4a89246be54232243a0450906186aea67467c8d378ec5eeb5bc9eefbf498aaad python-ovos-phal-plugin-homeassistant-df606469ed56965a3274ff5d7b62ec20c4b22f75.tar.gz diff --git a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.mk b/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.mk index 72b57058..85b65815 100644 --- a/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.mk +++ b/buildroot-external/package/python-ovos-phal-plugin-homeassistant/python-ovos-phal-plugin-homeassistant.mk @@ -4,7 +4,7 @@ # ################################################################################ -PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_VERSION = 5f07ec0053685c197e3da7dca617682a7e1e7ea5 +PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_VERSION = df606469ed56965a3274ff5d7b62ec20c4b22f75 PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_SITE = $(call github,OpenVoiceOS,ovos-PHAL-plugin-homeassistant,$(PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_VERSION)) PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_SETUP_TYPE = setuptools PYTHON_OVOS_PHAL_PLUGIN_HOMEASSISTANT_LICENSE_FILES = LICENSE