From d88acded11494b9cd1d44790ef17e933a5fc2662 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 17 Jun 2024 22:38:29 -0500 Subject: [PATCH 1/6] Fixes related to spelling, mypy, pylint --- README.md | 14 ++--- .../remote_homeassistant/__init__.py | 50 +++++++++------- .../remote_homeassistant/config_flow.py | 60 +++++++++++-------- .../remote_homeassistant/const.py | 2 +- .../remote_homeassistant/proxy_services.py | 10 ++-- 5 files changed, 78 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 149b8b5..51e0532 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![hacs][hacsbadge]][hacs] ![Project Maintenance][maintenance-shield] -App icon +App icon # Remote Home-Assistant @@ -57,11 +57,11 @@ This is not needed on the main instance. 1. Add a new Remote Home-Assistant integration - + 2. Specify the connection details to the remote instance - + You can generate an access token in the by logging into your remote instance, clicking on your user profile icon, and then selecting "Create Token" under "Long-Lived Access Tokens". @@ -69,17 +69,17 @@ Check "Secure" if you want to connect via a secure (https/wss) connection 3. After the instance is added, you can configure additional Options by clicking the "Options" button. - + 4. You can configure an optional prefix that gets prepended to all remote entities (if unsure, leave this blank). - + Click "Submit" to proceed to the next step. 5. You can also define filters, that include/exclude specified entities or domains from the remote instance. - + @@ -248,7 +248,7 @@ If you have remote domains (e.g. `switch`), that are not loaded on the main inst E.g. on the master: -``` +```yaml remote_homeassistant: instances: - host: 10.0.0.2 diff --git a/custom_components/remote_homeassistant/__init__.py b/custom_components/remote_homeassistant/__init__.py index 07bd9b1..1814872 100644 --- a/custom_components/remote_homeassistant/__init__.py +++ b/custom_components/remote_homeassistant/__init__.py @@ -4,7 +4,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/remote_homeassistant/ """ +from __future__ import annotations import asyncio +from typing import Optional import copy import fnmatch import inspect @@ -13,6 +15,7 @@ from contextlib import suppress import aiohttp +from aiohttp import ClientWebSocketResponse import homeassistant.components.websocket_api.auth as api import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -292,7 +295,7 @@ async def _update_listener(hass, config_entry): await hass.config_entries.async_reload(config_entry.entry_id) -class RemoteConnection(object): +class RemoteConnection: """A Websocket connection to a remote home-assistant instance.""" def __init__(self, hass, config_entry): @@ -328,7 +331,7 @@ def __init__(self, hass, config_entry): ) self._entity_prefix = config_entry.options.get(CONF_ENTITY_PREFIX, "") - self._connection = None + self._connection : Optional[ClientWebSocketResponse] = None self._heartbeat_task = None self._is_stopping = False self._entities = set() @@ -445,7 +448,7 @@ def _async_instance_id_match(info): async def _heartbeat_loop(self): """Send periodic heartbeats to remote instance.""" - while not self._connection.closed: + while self._connection is not None and not self._connection.closed: await asyncio.sleep(HEARTBEAT_INTERVAL) _LOGGER.debug("Sending ping") @@ -478,9 +481,13 @@ def _next_id(self): self.__id += 1 return _id - async def call(self, callback, message_type, **extra_args): + async def call(self, handler, message_type, **extra_args) -> None: + if self._connection is None: + _LOGGER.error("No remote websocket connection") + return + _id = self._next_id() - self._handlers[_id] = callback + self._handlers[_id] = handler try: await self._connection.send_json( {"id": _id, "type": message_type, **extra_args} @@ -511,7 +518,7 @@ async def _disconnected(self): asyncio.ensure_future(self.async_connect()) async def _recv(self): - while not self._connection.closed: + while self._connection is not None and not self._connection.closed: try: data = await self._connection.receive() except aiohttp.client_exceptions.ClientError as err: @@ -552,13 +559,13 @@ async def _recv(self): elif message["type"] == api.TYPE_AUTH_REQUIRED: if self._access_token: - data = {"type": api.TYPE_AUTH, "access_token": self._access_token} + json_data = {"type": api.TYPE_AUTH, "access_token": self._access_token} else: _LOGGER.error("Access token required, but not provided") self.set_connection_state(STATE_AUTH_REQUIRED) return try: - await self._connection.send_json(data) + await self._connection.send_json(json_data) except Exception as err: _LOGGER.error("could not send data to remote connection: %s", err) break @@ -570,12 +577,12 @@ async def _recv(self): return else: - callback = self._handlers.get(message["id"]) - if callback is not None: - if inspect.iscoroutinefunction(callback): - await callback(message) + handler = self._handlers.get(message["id"]) + if handler is not None: + if inspect.iscoroutinefunction(handler): + await handler(message) else: - callback(message) + handler(message) await self._disconnected() @@ -583,8 +590,8 @@ async def _init(self): async def forward_event(event): """Send local event to remote instance. - The affected entity_id has to origin from that remote instance, - otherwise the event is dicarded. + The affected entity_id has to originate from that remote instance, + otherwise the event is discarded. """ event_data = event.data service_data = event_data["service_data"] @@ -627,7 +634,10 @@ def _remove_prefix(entity_id): data = {"id": _id, "type": event.event_type, **event_data} _LOGGER.debug("forward event: %s", data) - + + if self._connection is None: + _LOGGER.error("There is no remote connecion to send send data to") + return try: await self._connection.send_json(data) except Exception as err: @@ -636,7 +646,7 @@ def _remove_prefix(entity_id): def state_changed(entity_id, state, attr): """Publish remote state change on local instance.""" - domain, object_id = split_entity_id(entity_id) + domain, _object_id = split_entity_id(entity_id) self._all_entity_names.add(entity_id) @@ -661,7 +671,7 @@ def state_changed(entity_id, state, attr): try: if f[CONF_BELOW] and float(state) < f[CONF_BELOW]: _LOGGER.info( - "%s: ignoring state '%s', because " "below '%s'", + "%s: ignoring state '%s', because below '%s'", entity_id, state, f[CONF_BELOW], @@ -669,7 +679,7 @@ def state_changed(entity_id, state, attr): return if f[CONF_ABOVE] and float(state) > f[CONF_ABOVE]: _LOGGER.info( - "%s: ignoring state '%s', because " "above '%s'", + "%s: ignoring state '%s', because above '%s'", entity_id, state, f[CONF_ABOVE], @@ -688,7 +698,7 @@ def state_changed(entity_id, state, attr): self._hass.states.async_set(entity_id, state, attr) def fire_event(message): - """Publish remove event on local instance.""" + """Publish remote event on local instance.""" if message["type"] == "result": return diff --git a/custom_components/remote_homeassistant/config_flow.py b/custom_components/remote_homeassistant/config_flow.py index a654beb..f982b59 100644 --- a/custom_components/remote_homeassistant/config_flow.py +++ b/custom_components/remote_homeassistant/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Remote Home-Assistant integration.""" +from __future__ import annotations import logging import enum +from typing import Any, Mapping from urllib.parse import urlparse @@ -31,11 +33,11 @@ FILTER_OPTIONS = [CONF_ENTITY_ID, CONF_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW] -def _filter_str(index, filter): - entity_id = filter[CONF_ENTITY_ID] - unit = filter[CONF_UNIT_OF_MEASUREMENT] - above = filter[CONF_ABOVE] - below = filter[CONF_BELOW] +def _filter_str(index, filter_conf: Mapping[str, str|float]): + entity_id = filter_conf[CONF_ENTITY_ID] + unit = filter_conf[CONF_UNIT_OF_MEASUREMENT] + above = filter_conf[CONF_ABOVE] + below = filter_conf[CONF_BELOW] return f"{index+1}. {entity_id}, unit: {unit}, above: {above}, below: {below}" @@ -50,8 +52,8 @@ async def validate_input(hass: core.HomeAssistant, conf): conf[CONF_ACCESS_TOKEN], conf.get(CONF_VERIFY_SSL, False), ) - except OSError: - raise CannotConnect() + except OSError as exc: + raise CannotConnect() from exc return {"title": info["location_name"], "uuid": info["uuid"]} @@ -91,9 +93,9 @@ async def async_step_user(self, user_input=None): elif user_input[CONF_TYPE] == CONF_MAIN: return await self.async_step_connection_details() - + errors["base"] = "unknown" - + return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -129,7 +131,7 @@ async def async_step_connection_details(self, user_input=None): self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) - user_input = user_input or dict() + user_input = user_input or {} host = user_input.get(CONF_HOST, self.prefill.get(CONF_HOST) or vol.UNDEFINED) port = user_input.get(CONF_PORT, self.prefill.get(CONF_PORT) or vol.UNDEFINED) secure = user_input.get(CONF_SECURE, self.prefill.get(CONF_SECURE) or vol.UNDEFINED) @@ -149,10 +151,10 @@ async def async_step_connection_details(self, user_input=None): errors=errors, ) - async def async_step_zeroconf(self, info): + async def async_step_zeroconf(self, discovery_info): """Handle instance discovered via zeroconf.""" - properties = info.properties - port = info.port + properties = discovery_info.properties + port = discovery_info.port uuid = properties["uuid"] await self.async_set_unique_id(uuid) @@ -203,11 +205,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize remote_homeassistant options flow.""" self.config_entry = config_entry - self.filters = None - self.events = None - self.options = None + self.filters : list[Any] | None = None + self.events : set[Any] | None = None + self.options : dict[str, Any] | None = None - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input : dict[str, str] | None = None): """Manage basic options.""" if self.config_entry.unique_id == REMOTE_ID: return self.async_abort(reason="not_supported") @@ -252,7 +254,7 @@ async def async_step_init(self, user_input=None): async def async_step_domain_entity_filters(self, user_input=None): """Manage domain and entity filters.""" - if user_input is not None: + if self.options is not None and user_input is not None: self.options.update(user_input) return await self.async_step_general_filters() @@ -289,21 +291,25 @@ async def async_step_general_filters(self, user_input=None): # Each filter string is prefixed with a number (index in self.filter+1). # Extract all of them and build the final filter list. selected_indices = [ - int(filter.split(".")[0]) - 1 - for filter in user_input.get(CONF_FILTER, []) + int(filterItem.split(".")[0]) - 1 + for filterItem in user_input.get(CONF_FILTER, []) ] - self.options[CONF_FILTER] = [self.filters[i] for i in selected_indices] + if self.options is not None: + self.options[CONF_FILTER] = [self.filters[i] for i in selected_indices] # type: ignore return await self.async_step_events() selected = user_input.get(CONF_FILTER, []) new_filter = {conf: user_input.get(conf) for conf in FILTER_OPTIONS} - selected.append(_filter_str(len(self.filters), new_filter)) - self.filters.append(new_filter) + + selected.append(_filter_str(len(self.filters), new_filter)) # type: ignore + self.filters.append(new_filter) # type: ignore else: self.filters = self.config_entry.options.get(CONF_FILTER, []) - selected = [_filter_str(i, filter) for i, filter in enumerate(self.filters)] + selected = [_filter_str(i, filterItem) for i, filterItem in enumerate(self.filters)] # type: ignore - strings = [_filter_str(i, filter) for i, filter in enumerate(self.filters)] + if self.filters is None: + self.filters = [] + strings = [_filter_str(i, filterItem) for i, filterItem in enumerate(self.filters)] return self.async_show_form( step_id="general_filters", data_schema=vol.Schema( @@ -322,13 +328,15 @@ async def async_step_general_filters(self, user_input=None): async def async_step_events(self, user_input=None): """Manage event options.""" if user_input is not None: - if ADD_NEW_EVENT not in user_input: + if ADD_NEW_EVENT not in user_input and self.options is not None: self.options[CONF_SUBSCRIBE_EVENTS] = user_input.get( CONF_SUBSCRIBE_EVENTS, [] ) return self.async_create_entry(title="", data=self.options) selected = user_input.get(CONF_SUBSCRIBE_EVENTS, []) + if self.events is None: + self.events = set() self.events.add(user_input[ADD_NEW_EVENT]) selected.append(user_input[ADD_NEW_EVENT]) else: diff --git a/custom_components/remote_homeassistant/const.py b/custom_components/remote_homeassistant/const.py index 618ce7e..88785b9 100644 --- a/custom_components/remote_homeassistant/const.py +++ b/custom_components/remote_homeassistant/const.py @@ -20,7 +20,7 @@ CONF_EXCLUDE_DOMAINS = "exclude_domains" CONF_EXCLUDE_ENTITIES = "exclude_entities" -# FIXME: There seems to be ne way to make these strings translateable +# FIXME: There seems to be no way to make these strings translateable CONF_MAIN = "Add a remote node" CONF_REMOTE = "Setup as remote node" diff --git a/custom_components/remote_homeassistant/proxy_services.py b/custom_components/remote_homeassistant/proxy_services.py index fcd5e40..40fb418 100644 --- a/custom_components/remote_homeassistant/proxy_services.py +++ b/custom_components/remote_homeassistant/proxy_services.py @@ -1,5 +1,7 @@ """Support for proxy services.""" +from __future__ import annotations import asyncio +from typing import Any import voluptuous as vol from homeassistant.exceptions import HomeAssistantError @@ -78,14 +80,14 @@ async def _async_got_services(self, message): self.registered_services.append((domain, service)) - async def _async_handle_service_call(self, event): + async def _async_handle_service_call(self, event) -> None: """Handle service call to proxy service.""" - # An eception must be raised from the service call handler (thus method) in + # An exception must be raised from the service call handler (thus method) in # order to end up in the frontend. The code below synchronizes reception of # the service call result, so potential error message can be used as exception # message. Not very pretty... ev = asyncio.Event() - res = None + res : dict[str,Any] | None = None def _resp(message): nonlocal res @@ -103,5 +105,5 @@ def _resp(message): ) await asyncio.wait_for(ev.wait(), SERVICE_CALL_LIMIT) - if not res["success"]: + if isinstance(res, dict) and not res["success"]: raise HomeAssistantError(res["error"]["message"]) From 6196f88093bc5678aa3499bb447da66a8af62c5d Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 17 Jun 2024 23:10:10 -0500 Subject: [PATCH 2/6] Pre-Commit Changes --- .github/workflows/hassfest.yml | 2 +- .github/workflows/validate.yml | 2 +- .vscode/settings.json | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml index fbe6d92..d305f95 100644 --- a/.github/workflows/hassfest.yml +++ b/.github/workflows/hassfest.yml @@ -10,6 +10,6 @@ jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v4" - uses: "home-assistant/actions/hassfest@master" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 38a71e0..8489d3a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,7 +10,7 @@ jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" + - uses: "actions/checkout@v4" - name: HACS validation uses: "hacs/action@main" with: diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2e9b3a6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} \ No newline at end of file From d5ffe57066ac21661bd14cba51c352f7b71c7621 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 18 Jun 2024 01:06:17 -0500 Subject: [PATCH 3/6] Fix manifest, services and add translations --- .../remote_homeassistant/manifest.json | 15 ++-- .../remote_homeassistant/services.yaml | 1 + .../remote_homeassistant/translations/de.json | 10 --- .../remote_homeassistant/translations/en.json | 10 --- .../translations/pt-BR.json | 10 --- .../translations/sensor.de.json | 12 +++ .../translations/sensor.en.json | 12 +++ .../translations/sensor.pt-BR.json | 12 +++ .../translations/sensor.sk.json | 12 +++ .../remote_homeassistant/translations/sk.json | 77 +++++++++++++++++++ 10 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 custom_components/remote_homeassistant/translations/sensor.de.json create mode 100644 custom_components/remote_homeassistant/translations/sensor.en.json create mode 100644 custom_components/remote_homeassistant/translations/sensor.pt-BR.json create mode 100644 custom_components/remote_homeassistant/translations/sensor.sk.json create mode 100644 custom_components/remote_homeassistant/translations/sk.json diff --git a/custom_components/remote_homeassistant/manifest.json b/custom_components/remote_homeassistant/manifest.json index 0ef19e0..a377abe 100644 --- a/custom_components/remote_homeassistant/manifest.json +++ b/custom_components/remote_homeassistant/manifest.json @@ -1,18 +1,19 @@ { "domain": "remote_homeassistant", "name": "Remote Home-Assistant", - "issue_tracker": "https://github.com/custom-components/remote_homeassistant/issues", - "documentation": "https://github.com/custom-components/remote_homeassistant", - "dependencies": ["http"], - "config_flow": true, + "version": "2024.06.18", "codeowners": [ + "@jaym25", "@lukas-hetzenecker", "@postlund" ], + "config_flow": true, + "dependencies": ["http"], + "documentation": "https://github.com/custom-components/remote_homeassistant", + "iot_class": "local_push", + "issue_tracker": "https://github.com/custom-components/remote_homeassistant/issues", "requirements": [], "zeroconf": [ "_home-assistant._tcp.local." - ], - "version": "3.11", - "iot_class": "local_push" + ] } diff --git a/custom_components/remote_homeassistant/services.yaml b/custom_components/remote_homeassistant/services.yaml index b0e3f52..641400b 100644 --- a/custom_components/remote_homeassistant/services.yaml +++ b/custom_components/remote_homeassistant/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload Remote Home-Assistant description: Reload remote_homeassistant and re-process yaml configuration. diff --git a/custom_components/remote_homeassistant/translations/de.json b/custom_components/remote_homeassistant/translations/de.json index b6961bf..2c75dd0 100644 --- a/custom_components/remote_homeassistant/translations/de.json +++ b/custom_components/remote_homeassistant/translations/de.json @@ -30,16 +30,6 @@ "already_configured": "Bereits konfiguriert" } }, - "state": { - "_": { - "disconnected": "Getrennt", - "connecting": "Verbindet", - "connected": "Verbunden", - "reconnecting": "Wiederverbinden", - "auth_invalid": "Ungültiger Zugangstoken", - "auth_required": "Authentifizierung erforderlich" - } - }, "options": { "step": { "init": { diff --git a/custom_components/remote_homeassistant/translations/en.json b/custom_components/remote_homeassistant/translations/en.json index ef42f78..3d287c9 100644 --- a/custom_components/remote_homeassistant/translations/en.json +++ b/custom_components/remote_homeassistant/translations/en.json @@ -30,16 +30,6 @@ "already_configured": "Already configured" } }, - "state": { - "_": { - "disconnected": "Disconnected", - "connecting": "Connecting", - "connected": "Connected", - "reconnecting": "Re-connecting", - "auth_invalid": "Invalid access token", - "auth_required": "Authentication Required" - } - }, "options": { "step": { "init": { diff --git a/custom_components/remote_homeassistant/translations/pt-BR.json b/custom_components/remote_homeassistant/translations/pt-BR.json index 9e6af41..13e2899 100644 --- a/custom_components/remote_homeassistant/translations/pt-BR.json +++ b/custom_components/remote_homeassistant/translations/pt-BR.json @@ -30,16 +30,6 @@ "already_configured": "Já configurado" } }, - "state": { - "_": { - "disconnected": "Desconectado", - "connecting": "Conectando", - "connected": "Conectado", - "reconnecting": "Reconectando", - "auth_invalid": "Token de acesso inválido", - "auth_required": "Autentificação requerida" - } - }, "options": { "step": { "init": { diff --git a/custom_components/remote_homeassistant/translations/sensor.de.json b/custom_components/remote_homeassistant/translations/sensor.de.json new file mode 100644 index 0000000..efe2f37 --- /dev/null +++ b/custom_components/remote_homeassistant/translations/sensor.de.json @@ -0,0 +1,12 @@ +{ + "state": { + "remote_homeassistant___": { + "disconnected": "Getrennt", + "connecting": "Verbindet", + "connected": "Verbunden", + "reconnecting": "Wiederverbinden", + "auth_invalid": "Ungültiger Zugangstoken", + "auth_required": "Authentifizierung erforderlich" + } + } +} \ No newline at end of file diff --git a/custom_components/remote_homeassistant/translations/sensor.en.json b/custom_components/remote_homeassistant/translations/sensor.en.json new file mode 100644 index 0000000..062e2a9 --- /dev/null +++ b/custom_components/remote_homeassistant/translations/sensor.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "remote_homeassistant___": { + "disconnected": "Disconnected", + "connecting": "Connecting", + "connected": "Connected", + "reconnecting": "Re-connecting", + "auth_invalid": "Invalid access token", + "auth_required": "Authentication Required" + } + } +} \ No newline at end of file diff --git a/custom_components/remote_homeassistant/translations/sensor.pt-BR.json b/custom_components/remote_homeassistant/translations/sensor.pt-BR.json new file mode 100644 index 0000000..aed064e --- /dev/null +++ b/custom_components/remote_homeassistant/translations/sensor.pt-BR.json @@ -0,0 +1,12 @@ +{ + "state": { + "remote_homeassistant___": { + "disconnected": "Desconectado", + "connecting": "Conectando", + "connected": "Conectado", + "reconnecting": "Reconectando", + "auth_invalid": "Token de acesso inválido", + "auth_required": "Autentificação requerida" + } + } +} \ No newline at end of file diff --git a/custom_components/remote_homeassistant/translations/sensor.sk.json b/custom_components/remote_homeassistant/translations/sensor.sk.json new file mode 100644 index 0000000..daca7de --- /dev/null +++ b/custom_components/remote_homeassistant/translations/sensor.sk.json @@ -0,0 +1,12 @@ +{ + "state": { + "remote_homeassistant___": { + "disconnected": "Odpojené", + "connecting": "Pripája sa", + "connected": "Pripojené", + "reconnecting": "Opätovné pripojenie", + "auth_invalid": "Neplatný prístupový token", + "auth_required": "Vyžaduje sa overenie" + } + } +} \ No newline at end of file diff --git a/custom_components/remote_homeassistant/translations/sk.json b/custom_components/remote_homeassistant/translations/sk.json new file mode 100644 index 0000000..4b94739 --- /dev/null +++ b/custom_components/remote_homeassistant/translations/sk.json @@ -0,0 +1,77 @@ +{ + "config": { + "flow_title": "Diaľkové ovládanie: {name}", + "step": { + "user": { + "title": "Vyberte typ inštalácie", + "description": "Vzdialený uzol je inštancia, z ktorej sa zhromažďujú stavy" + }, + "connection_details": { + "title": "Podrobnosti pripojenia", + "data": { + "host": "Host", + "port": "Port", + "secure": "Zabezpečiť", + "verify_ssl": "Overiť SSL", + "access_token": "Prístupový token", + "max_message_size": "Maximálna veľkosť správy" + } + } + }, + "error": { + "api_problem": "Zlá odpoveď zo servera", + "cannot_connect": "Nepodarilo sa pripojiť k serveru", + "invalid_auth": "Neplatné poverenia", + "unsupported_version": "Nepodporovaná verzia. Vyžaduje sa aspoň verzia 0.111.", + "unknown": "Vyskytla sa neznáma chyba", + "missing_endpoint": "Na tohto hostiteľa si musíte nainštalovať Remote Home Assistant a do jeho konfigurácie pridať remote_homeassistant:." + }, + "abort": { + "already_configured": "Už je nakonfigurovaný" + } + }, + "options": { + "step": { + "init": { + "title": "Základné možnosti (krok 1/4)", + "data": { + "entity_prefix": "Predpona entity (voliteľné)", + "load_components": "Načítať komponent (ak nie je načítaný)", + "service_prefix": "Predpona služby", + "services": "Vzdialené služby" + } + }, + "domain_entity_filters": { + "title": "Filtre domén a entít (krok 2/4)", + "data": { + "include_domains": "Zahrnúť domény", + "include_entities": "Zahrnúť entity", + "exclude_domains": "Vylúčiť domény", + "exclude_entities": "Vylúčiť entity" + } + }, + "general_filters": { + "title": "Filtre (krok 3/4)", + "description": "Zadajte nový filter `Entity ID`, jeden alebo viac atribútov filtra a stlačte `Submit`. Odstráňte existujúce filtre tak, že ich zrušíte `Filters`.\n\nOpustiť `Entity ID` vyprázdnite a stlačte `Submit` aby ste nevykonali žiadne ďalšie zmeny.", + "data": { + "filter": "Filtre", + "entity_id": "Entity ID", + "unit_of_measurement": "Jednotka merania", + "above": "Nad", + "below": "Pod" + } + }, + "events": { + "title": "Odoberané udalosti (krok 4/4)", + "description": "Pridajte novú odoberanú udalosť zadaním jej názvu `Add new event` a stlačiť `Submit`. Odstráňte existujúce udalosti zrušením ich začiarknutia `Events`.\n\nOpustiť `Add new event` a stlačiť `Submit` aby ste nevykonali žiadne ďalšie zmeny.", + "data": { + "subscribe_events": "Udalosti", + "add_new_event": "Pridať novú udalosť" + } + } + }, + "abort": { + "not_supported": "Pre vzdialený uzol nie sú podporované žiadne možnosti konfigurácie" + } + } + } \ No newline at end of file From d084c982fb1b7aa12b230185117fc8803b90141d Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 18 Jun 2024 01:27:33 -0500 Subject: [PATCH 4/6] Add local unique id --- .../remote_homeassistant/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/custom_components/remote_homeassistant/__init__.py b/custom_components/remote_homeassistant/__init__.py index 1814872..8593512 100644 --- a/custom_components/remote_homeassistant/__init__.py +++ b/custom_components/remote_homeassistant/__init__.py @@ -31,9 +31,11 @@ from homeassistant.core import (Context, EventOrigin, HomeAssistant, callback, split_entity_id) from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.reload import async_integration_yaml_config +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.setup import async_setup_component @@ -485,7 +487,7 @@ async def call(self, handler, message_type, **extra_args) -> None: if self._connection is None: _LOGGER.error("No remote websocket connection") return - + _id = self._next_id() self._handlers[_id] = handler try: @@ -690,6 +692,17 @@ def state_changed(entity_id, state, attr): entity_id = self._prefixed_entity_id(entity_id) + # Add local unique id + domain, object_id = split_entity_id(entity_id) + attr['unique_id'] = f"{self._entry.unique_id[:16]}_{entity_id}" + entity_registry = er.async_get(self._hass) + entity_registry.async_get_or_create( + domain=domain, + platform='remote_homeassistant', + unique_id=attr['unique_id'], + suggested_object_id=object_id, + ) + # Add local customization data if DATA_CUSTOMIZE in self._hass.data: attr.update(self._hass.data[DATA_CUSTOMIZE].get(entity_id)) From 5e517708e83605e0d11748cbce175683bb53fb41 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 18 Jun 2024 01:53:03 -0500 Subject: [PATCH 5/6] Fix deprecated errors from core 2024-5-1 --- custom_components/remote_homeassistant/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/remote_homeassistant/__init__.py b/custom_components/remote_homeassistant/__init__.py index 8593512..938e68f 100644 --- a/custom_components/remote_homeassistant/__init__.py +++ b/custom_components/remote_homeassistant/__init__.py @@ -36,7 +36,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from custom_components.remote_homeassistant.views import DiscoveryInfoView @@ -187,11 +187,11 @@ async def _async_update_config_entry_if_from_yaml(hass, entries_by_id, conf): hass.config_entries.async_update_entry(entry, data=data, options=options) -async def setup_remote_instance(hass: HomeAssistantType): +async def setup_remote_instance(hass: HomeAssistant.core.HomeAssistant): hass.http.register_view(DiscoveryInfoView()) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant.core.HomeAssistant, config: ConfigType): """Set up the remote_homeassistant component.""" hass.data.setdefault(DOMAIN, {}) @@ -215,7 +215,7 @@ async def _handle_reload(service): hass.async_create_task(setup_remote_instance(hass)) - hass.helpers.service.async_register_admin_service( + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _handle_reload, From e3d6b96659e5275f4df75fdf0750334709d14bb4 Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 18 Jun 2024 04:37:15 -0500 Subject: [PATCH 6/6] Add optional entity friendly name prefix --- README.md | 1 + .../remote_homeassistant/__init__.py | 44 ++++++++++++++++++- .../remote_homeassistant/config_flow.py | 9 ++++ .../remote_homeassistant/const.py | 1 + .../remote_homeassistant/sensor.py | 7 ++- .../remote_homeassistant/translations/de.json | 1 + .../remote_homeassistant/translations/en.json | 1 + .../translations/pt-BR.json | 1 + .../remote_homeassistant/translations/sk.json | 1 + 9 files changed, 62 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 51e0532..ae28658 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The main instance connects to the Websocket APIs of the remote instances (alread After the connection is completed, the remote states get populated into the master instance. The entity ids can optionally be prefixed via the `entity_prefix` parameter. +The entity friendly names can optionally be prefixed via the `entity_friendly_name_prefix` parameter. The component keeps track which objects originate from which instance. Whenever a service is called on an object, the call gets forwarded to the particular remote instance. diff --git a/custom_components/remote_homeassistant/__init__.py b/custom_components/remote_homeassistant/__init__.py index 938e68f..68b9d95 100644 --- a/custom_components/remote_homeassistant/__init__.py +++ b/custom_components/remote_homeassistant/__init__.py @@ -57,6 +57,7 @@ CONF_SECURE = "secure" CONF_SUBSCRIBE_EVENTS = "subscribe_events" CONF_ENTITY_PREFIX = "entity_prefix" +CONF_ENTITY_FRIENDLY_NAME_PREFIX = "entity_friendly_name_prefix" CONF_FILTER = "filter" CONF_MAX_MSG_SIZE = "max_message_size" @@ -69,6 +70,7 @@ STATE_DISCONNECTED = "disconnected" DEFAULT_ENTITY_PREFIX = "" +DEFAULT_ENTITY_FRIENDLY_NAME_PREFIX = "" INSTANCES_SCHEMA = vol.Schema( { @@ -108,7 +110,10 @@ ], ), vol.Optional(CONF_SUBSCRIBE_EVENTS): cv.ensure_list, - vol.Optional(CONF_ENTITY_PREFIX, default=DEFAULT_ENTITY_PREFIX): cv.string, + vol.Optional(CONF_ENTITY_PREFIX, + default=DEFAULT_ENTITY_PREFIX): cv.string, + vol.Optional(CONF_ENTITY_FRIENDLY_NAME_PREFIX, + default=DEFAULT_ENTITY_FRIENDLY_NAME_PREFIX): cv.string, vol.Optional(CONF_LOAD_COMPONENTS): cv.ensure_list, vol.Required(CONF_SERVICE_PREFIX, default="remote_"): cv.string, vol.Optional(CONF_SERVICES): cv.ensure_list, @@ -157,6 +162,7 @@ def async_yaml_to_config_entry(instance_conf): CONF_FILTER, CONF_SUBSCRIBE_EVENTS, CONF_ENTITY_PREFIX, + CONF_ENTITY_FRIENDLY_NAME_PREFIX, CONF_LOAD_COMPONENTS, CONF_SERVICE_PREFIX, CONF_SERVICES, @@ -331,7 +337,10 @@ def __init__(self, hass, config_entry): self._subscribe_events = set( config_entry.options.get(CONF_SUBSCRIBE_EVENTS, []) + INTERNALLY_USED_EVENTS ) - self._entity_prefix = config_entry.options.get(CONF_ENTITY_PREFIX, "") + self._entity_prefix = config_entry.options.get( + CONF_ENTITY_PREFIX, "") + self._entity_friendly_name_prefix = config_entry.options.get( + CONF_ENTITY_FRIENDLY_NAME_PREFIX, "") self._connection : Optional[ClientWebSocketResponse] = None self._heartbeat_task = None @@ -354,6 +363,26 @@ def _prefixed_entity_id(self, entity_id): return entity_id return entity_id + def _prefixed_entity_friendly_name(self, entity_friendly_name): + if (self._entity_friendly_name_prefix + and entity_friendly_name.startswith(self._entity_friendly_name_prefix) + == False): + entity_friendly_name = (self._entity_friendly_name_prefix + + entity_friendly_name) + return entity_friendly_name + return entity_friendly_name + + def _full_picture_url(self, url): + baseURL = "%s://%s:%s" % ( + "https" if self._secure else "http", + self._entry.data[CONF_HOST], + self._entry.data[CONF_PORT], + ) + if url.startswith(baseURL) == False: + url = baseURL + url + return url + return url + def set_connection_state(self, state): """Change current connection state.""" signal = f"remote_homeassistant_{self._entry.unique_id}" @@ -707,6 +736,12 @@ def state_changed(entity_id, state, attr): if DATA_CUSTOMIZE in self._hass.data: attr.update(self._hass.data[DATA_CUSTOMIZE].get(entity_id)) + for attrId, value in attr.items(): + if attrId == "friendly_name": + attr[attrId] = self._prefixed_entity_friendly_name(value) + if attrId == "entity_picture": + attr[attrId] = self._full_picture_url(value) + self._entities.add(entity_id) self._hass.states.async_set(entity_id, state, attr) @@ -753,6 +788,11 @@ def got_states(message): entity_id = entity["entity_id"] state = entity["state"] attributes = entity["attributes"] + for attr, value in attributes.items(): + if attr == "friendly_name": + attributes[attr] = self._prefixed_entity_friendly_name(value) + if attr == "entity_picture": + attributes[attr] = self._full_picture_url(value) state_changed(entity_id, state, attributes) diff --git a/custom_components/remote_homeassistant/config_flow.py b/custom_components/remote_homeassistant/config_flow.py index f982b59..ced5f2d 100644 --- a/custom_components/remote_homeassistant/config_flow.py +++ b/custom_components/remote_homeassistant/config_flow.py @@ -18,6 +18,7 @@ from . import async_yaml_to_config_entry from .const import (CONF_ENTITY_PREFIX, # pylint:disable=unused-import + CONF_ENTITY_FRIENDLY_NAME_PREFIX, CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, CONF_FILTER, CONF_INCLUDE_DOMAINS, CONF_INCLUDE_ENTITIES, CONF_LOAD_COMPONENTS, CONF_MAIN, CONF_OPTIONS, CONF_REMOTE, CONF_REMOTE_CONNECTION, @@ -237,6 +238,14 @@ async def async_step_init(self, user_input : dict[str, str] | None = None): ) }, ): str, + vol.Optional( + CONF_ENTITY_FRIENDLY_NAME_PREFIX, + description={ + "suggested_value": self.config_entry.options.get( + CONF_ENTITY_FRIENDLY_NAME_PREFIX + ) + }, + ): str, vol.Optional( CONF_LOAD_COMPONENTS, default=self._default(CONF_LOAD_COMPONENTS), diff --git a/custom_components/remote_homeassistant/const.py b/custom_components/remote_homeassistant/const.py index 88785b9..fad2f2c 100644 --- a/custom_components/remote_homeassistant/const.py +++ b/custom_components/remote_homeassistant/const.py @@ -13,6 +13,7 @@ CONF_API_PASSWORD = "api_password" CONF_SUBSCRIBE_EVENTS = "subscribe_events" CONF_ENTITY_PREFIX = "entity_prefix" +CONF_ENTITY_FRIENDLY_NAME_PREFIX = "entity_friendly_name_prefix" CONF_MAX_MSG_SIZE = "max_message_size" CONF_INCLUDE_DOMAINS = "include_domains" diff --git a/custom_components/remote_homeassistant/sensor.py b/custom_components/remote_homeassistant/sensor.py index 117b396..f128195 100644 --- a/custom_components/remote_homeassistant/sensor.py +++ b/custom_components/remote_homeassistant/sensor.py @@ -3,8 +3,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import DOMAIN, CONF_ENTITY_PREFIX, CONF_SECURE, CONF_MAX_MSG_SIZE, DEFAULT_MAX_MSG_SIZE - +from .const import (DOMAIN, CONF_ENTITY_PREFIX, + CONF_ENTITY_FRIENDLY_NAME_PREFIX, + CONF_SECURE, CONF_MAX_MSG_SIZE, + DEFAULT_MAX_MSG_SIZE) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensor based ok config entry.""" @@ -46,6 +48,7 @@ def extra_state_attributes(self): "verify_ssl": self._entry.data.get(CONF_VERIFY_SSL, False), "max_msg_size": self._entry.data.get(CONF_MAX_MSG_SIZE, DEFAULT_MAX_MSG_SIZE), "entity_prefix": self._entry.options.get(CONF_ENTITY_PREFIX, ""), + "entity_friendly_name_prefix": self._entry.options.get(CONF_ENTITY_FRIENDLY_NAME_PREFIX, ""), "uuid": self.unique_id, } diff --git a/custom_components/remote_homeassistant/translations/de.json b/custom_components/remote_homeassistant/translations/de.json index 2c75dd0..fe7d1ae 100644 --- a/custom_components/remote_homeassistant/translations/de.json +++ b/custom_components/remote_homeassistant/translations/de.json @@ -36,6 +36,7 @@ "title": "Basis-Einstellungen (Schritt 1/4)", "data": { "entity_prefix": "Entitätspräfix (optional)", + "entity_friendly_name_prefix": "Entitätsname präfix (optional)", "load_components": "Komponente laden (wenn nicht geladen)", "service_prefix": "Servicepräfix", "services": "Remote Services" diff --git a/custom_components/remote_homeassistant/translations/en.json b/custom_components/remote_homeassistant/translations/en.json index 3d287c9..e98e968 100644 --- a/custom_components/remote_homeassistant/translations/en.json +++ b/custom_components/remote_homeassistant/translations/en.json @@ -36,6 +36,7 @@ "title": "Basic Options (step 1/4)", "data": { "entity_prefix": "Entity prefix (optional)", + "entity_friendly_name_prefix": "Entity name prefix (optional)", "load_components": "Load component (if not loaded)", "service_prefix": "Service prefix", "services": "Remote Services" diff --git a/custom_components/remote_homeassistant/translations/pt-BR.json b/custom_components/remote_homeassistant/translations/pt-BR.json index 13e2899..04352ea 100644 --- a/custom_components/remote_homeassistant/translations/pt-BR.json +++ b/custom_components/remote_homeassistant/translations/pt-BR.json @@ -36,6 +36,7 @@ "title": "Opções básicas (passo 1/4)", "data": { "entity_prefix": "Prefixo da entidade (opcional)", + "entity_friendly_name_prefix": "Prefixo da entidade nombre (opcional)", "load_components": "Carregar componente (se não estiver carregado)", "service_prefix": "Prefixo do serviço", "services": "Serviços remotos" diff --git a/custom_components/remote_homeassistant/translations/sk.json b/custom_components/remote_homeassistant/translations/sk.json index 4b94739..56cd4eb 100644 --- a/custom_components/remote_homeassistant/translations/sk.json +++ b/custom_components/remote_homeassistant/translations/sk.json @@ -36,6 +36,7 @@ "title": "Základné možnosti (krok 1/4)", "data": { "entity_prefix": "Predpona entity (voliteľné)", + "entity_friendly_name_prefix": "Predpona entity name (voliteľné)", "load_components": "Načítať komponent (ak nie je načítaný)", "service_prefix": "Predpona služby", "services": "Vzdialené služby"