diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index a1be8f5..a9b5b27 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -11,5 +11,5 @@ 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.yaml b/.github/workflows/validate.yaml index db8f82e..9cb7d34 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -1,18 +1,19 @@ -name: Validate +name: Validate HACS on: + #push: + #pull_request: + #schedule: + # - cron: "0 0 * * *" workflow_dispatch: -# push: -# pull_request: -# schedule: -# - cron: "0 0 * * *" + +permissions: {} jobs: - validate: + validate-hacs: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" - name: HACS validation uses: "hacs/action@main" with: - category: "integration" + category: "integration" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 46a6b5c..9bdfa1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 LAB02 Research +Copyright (c) 2023 HASS.Agent Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/custom_components/hass_agent/__init__.py b/custom_components/hass_agent/__init__.py index b44717d..daca63f 100644 --- a/custom_components/hass_agent/__init__.py +++ b/custom_components/hass_agent/__init__.py @@ -1,4 +1,5 @@ """The HASS.Agent integration.""" + from __future__ import annotations import asyncio @@ -21,23 +22,24 @@ ) from homeassistant.const import ( CONF_ID, - CONF_NAME, - CONF_URL, - Platform, + CONF_NAME, + CONF_URL, + Platform, SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant, callback, ServiceCall, async_get_hass from homeassistant.helpers import device_registry as dr from homeassistant.helpers import discovery from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify +from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN, CONF_ORIGINAL_DEVICE_NAME, CONF_DEVICE_NAME PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER] _logger = logging.getLogger(__name__) + async def update_device_info(hass: HomeAssistant, entry: ConfigEntry, new_device_info): device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -49,43 +51,12 @@ async def update_device_info(hass: HomeAssistant, entry: ConfigEntry, new_device sw_version=new_device_info["device"]["sw_version"], ) -async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool: - """Wait for the MQTT client to become available. - Waits when mqtt set up is in progress, - It is not needed that the client is connected. - Returns True if the mqtt client is available. - Returns False when the client is not available. - """ - if not mqtt_config_entry_enabled(hass): - return False - - entry = hass.config_entries.async_entries(DOMAIN)[0] - if entry.state == ConfigEntryState.LOADED: - return True - - state_reached_future: asyncio.Future[bool] - if DATA_MQTT_AVAILABLE not in hass.data: - hass.data[DATA_MQTT_AVAILABLE] = state_reached_future = asyncio.Future() - else: - state_reached_future = hass.data[DATA_MQTT_AVAILABLE] - if state_reached_future.done(): - return state_reached_future.result() - - try: - async with async_timeout.timeout(AVAILABILITY_TIMEOUT): - # Await the client setup or an error state was received - return await state_reached_future - except asyncio.TimeoutError: - return False async def handle_apis_changed(hass: HomeAssistant, entry: ConfigEntry, apis): _logger.debug("api changed for: %s", entry.unique_id) if apis is not None: - device_registry = dr.async_get(hass) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.unique_id)} - ) + device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) media_player = apis.get("media_player", False) is_media_player_loaded = hass.data[DOMAIN][entry.entry_id]["loaded"]["media_player"] @@ -100,39 +71,51 @@ async def handle_apis_changed(hass: HomeAssistant, entry: ConfigEntry, apis): hass.data[DOMAIN][entry.entry_id]["loaded"]["media_player"] = True else: if is_media_player_loaded: - _logger.debug("unloading media player for device: %s [%s]", device.name, entry.unique_id) - await hass.config_entries.async_forward_entry_unload( - entry, Platform.MEDIA_PLAYER + _logger.debug( + "unloading media player for device: %s [%s]", + device.name, + entry.unique_id, ) + await hass.config_entries.async_forward_entry_unload(entry, Platform.MEDIA_PLAYER) hass.data[DOMAIN][entry.entry_id]["loaded"]["media_player"] = False if notifications and is_notifications_loaded is False: - _logger.debug("loading notifications for device: %s [%s]", device.name, entry.unique_id) + _logger.debug( + "loading notifications for device: %s [%s]", + device.name, + entry.unique_id, + ) + + original_device_name = entry.data.get(CONF_ORIGINAL_DEVICE_NAME, device.name) hass.async_create_task( discovery.async_load_platform( hass, Platform.NOTIFY, DOMAIN, - {CONF_ID: entry.entry_id, CONF_NAME: device.name}, + { + CONF_ID: entry.entry_id, + CONF_NAME: original_device_name, # Note(Amadeo): CONF_NAME decides of "nofity." name, needs to be set to the original one + CONF_DEVICE_NAME: device.name, # Note(Amadeo): since CONF_NAME is used for the old name, we need to pass on the changed name for MQTT notify call + }, {}, ) ) hass.data[DOMAIN][entry.entry_id]["loaded"]["notifications"] = True else: if is_notifications_loaded: - _logger.debug("unloading notifications for device: %s [%s]", device.name, entry.unique_id) - await hass.config_entries.async_unload_platforms( - entry, [Platform.NOTIFY] - ) + # _logger.debug("unloading notifications for device: %s [%s]", device.name, entry.unique_id) + # await hass.config_entries.async_unload_platforms(entry, [Platform.NOTIFY]) + # NOTE(Amadeo): disabled due to "ValueError: Config entry was never loaded!" error hass.data[DOMAIN][entry.entry_id]["loaded"]["notifications"] = False + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up HASS.Agent from a config entry.""" - _logger.debug("setting up device from config entry: %s [%s]", entry.data["device"]["name"], entry.unique_id) + _logger.debug("setting up device from config entry: %s [%s]", entry.title, entry.unique_id) hass.data.setdefault(DOMAIN, {}) @@ -164,7 +147,7 @@ def get_device_info(): "media_player": False, # unsupported for the moment } - hass.async_create_background_task(handle_apis_changed(hass, entry, apis)) + hass.async_create_background_task(handle_apis_changed(hass, entry, apis), "hass.agent-api") hass.data[DOMAIN][entry.entry_id]["apis"] = apis else: @@ -174,6 +157,10 @@ def get_device_info(): @callback async def updated(message: ReceiveMessage): + if not message.payload: + _logger.debug("received empty update message on '%s', ignoring", message.topic) + return + payload = json.loads(message.payload) cached = hass.data[DOMAIN][entry.entry_id]["apis"] apis = payload["apis"] @@ -202,44 +189,39 @@ async def updated(message: ReceiveMessage): return True + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - deviceName = entry.data["device"]["name"] - - _logger.debug("unloading device: %s [%s]", deviceName, entry.unique_id) + _logger.debug("unloading device: %s [%s]", entry.title, entry.unique_id) # known issue: notify does not always unload - + # NOTE(Amadeo): unloading NOTIFY platform always fails, same happens for example for https://github.com/home-assistant/core/blob/dd7f7be6adee76f2add98dcca8d3ff87bceabf70/homeassistant/components/nfandroidtv/__init__.py loaded = hass.data[DOMAIN][entry.entry_id].get("loaded", None) if loaded is not None: notifications = loaded.get("notifications", False) media_player = loaded.get("media_player", False) - if notifications: - if unload_ok := await hass.config_entries.async_unload_platforms( - entry, [Platform.NOTIFY] - ): - _logger.debug("unloaded notifications for: %s [%s]", deviceName, entry.unique_id) + # if notifications: + # if unload_ok := await hass.config_entries.async_unload_platforms(entry, [Platform.NOTIFY]): + # _logger.debug("unloaded notifications for: %s [%s]", entry.title, entry.unique_id) + # NOTE(Amadeo): disabled due to "ValueError: Config entry was never loaded!" error if media_player: - if unload_ok := await hass.config_entries.async_unload_platforms( - entry, [Platform.MEDIA_PLAYER] - ): - _logger.debug("unloaded media player for: %s [%s]", deviceName, entry.unique_id) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, [Platform.MEDIA_PLAYER]): + _logger.debug("unloaded media player for: %s [%s]", entry.title, entry.unique_id) else: _logger.warning("config entry (%s) with has no apis loaded?", entry.unique_id) url = entry.data.get(CONF_URL, None) if url is None: - async_unsubscribe_topics( - hass, hass.data[DOMAIN][entry.entry_id]["internal_mqtt"] - ) + async_unsubscribe_topics(hass, hass.data[DOMAIN][entry.entry_id]["internal_mqtt"]) hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return True + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up hass_agent integration.""" @@ -248,28 +230,4 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(MediaPlayerThumbnailView(hass)) - # Make sure MQTT integration is enabled and the client is available - if not await mqtt.async_wait_for_mqtt_client(hass): - _logger.error("MQTT integration is not available") - return False - - # async def _handle_reload(service): - # """Handle reload service call.""" - # _logger.info("Service %s.reload called: reloading integration", DOMAIN) - - # current_entries = hass.config_entries.async_entries(DOMAIN) - - # reload_tasks = [ - # hass.config_entries.async_reload(entry.entry_id) - # for entry in current_entries - # ] - - # await asyncio.gather(*reload_tasks) - - # hass.services.async_register( - # DOMAIN, - # SERVICE_RELOAD, - # _handle_reload, - # ) - return True diff --git a/custom_components/hass_agent/config_flow.py b/custom_components/hass_agent/config_flow.py index 8ee0994..67d3d1a 100644 --- a/custom_components/hass_agent/config_flow.py +++ b/custom_components/hass_agent/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HASS.Agent""" + from __future__ import annotations import json import logging @@ -13,25 +14,21 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, CONF_URL from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.service_info.mqtt import MqttServiceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import DOMAIN, CONF_DEFAULT_NOTIFICATION_TITLE +from .const import DOMAIN, CONF_DEFAULT_NOTIFICATION_TITLE, CONF_ORIGINAL_DEVICE_NAME, CONF_DEVICE_NAME _logger = logging.getLogger(__name__) class OptionsFlowHandler(config_entries.OptionsFlow): - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self) -> None: """Initialize options flow.""" - self.config_entry = config_entry - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Manage the options.""" if user_input is not None: - user_input[CONF_DEFAULT_NOTIFICATION_TITLE] = user_input[ - CONF_DEFAULT_NOTIFICATION_TITLE - ].strip() + user_input[CONF_DEFAULT_NOTIFICATION_TITLE] = user_input[CONF_DEFAULT_NOTIFICATION_TITLE].strip() return self.async_create_entry(title="", data=user_input) @@ -41,9 +38,7 @@ async def async_step_init( { vol.Optional( CONF_DEFAULT_NOTIFICATION_TITLE, - default=self.config_entry.options.get( - CONF_DEFAULT_NOTIFICATION_TITLE, ATTR_TITLE_DEFAULT - ), + default=self.config_entry.options.get(CONF_DEFAULT_NOTIFICATION_TITLE, ATTR_TITLE_DEFAULT), ): str } ), @@ -66,27 +61,56 @@ def async_get_options_flow( config_entry: config_entries.ConfigEntry, ) -> config_entries.OptionsFlow: """Create the options flow.""" - return OptionsFlowHandler(config_entry) + return OptionsFlowHandler() async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Handle a flow initialized by MQTT discovery.""" - device_name = discovery_info.topic.split("hass.agent/devices/")[1] + if not discovery_info.payload: + _logger.debug( + "received empty discovery message on '%s', ignoring", + discovery_info.topic, + ) + return self.async_abort(reason="not_supported") payload = json.loads(discovery_info.payload) + device_name = payload["device"]["name"] serial_number = payload["serial_number"] _logger.debug("found device. Name: %s, Serial Number: %s", device_name, serial_number) self._data = {"device": payload["device"], "apis": payload["apis"]} - for config in self._async_current_entries(): - _logger.debug("device: %s, SN: %s, UID: %s", device_name, serial_number, config.unique_id) # TODO(Amadeo): remove - if config.unique_id == serial_number: - _logger.debug("device %s, serial number: %s already configured, ignoring", device_name, serial_number) - return self.async_abort(reason="already_configured") + entry = await self.async_set_unique_id(serial_number) + if not entry or (CONF_ORIGINAL_DEVICE_NAME not in entry.data): + self._data[CONF_ORIGINAL_DEVICE_NAME] = device_name + + if entry: + reload_required = device_name != entry.title + + self.hass.config_entries.async_update_entry( + entry, + title=payload["device"]["name"], + data={**entry.data, **self._data}, + ) + + if reload_required: + self.hass.config_entries.async_schedule_reload(entry.entry_id) + + async_create_issue( + hass=self.hass, + domain=DOMAIN, + issue_id=f"restart_required_{device_name}", + data={CONF_DEVICE_NAME: device_name}, + is_fixable=True, + severity=IssueSeverity.WARNING, + translation_key="restart_required", + translation_placeholders={ + "name": device_name, + }, + ) - await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() # "hass.agent/devices/#" is hardcoded in HASS.Agent's manifest assert discovery_info.subscribed_topic == "hass.agent/devices/#" @@ -95,10 +119,7 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: return await self.async_step_confirm() - async def async_step_local_api( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - + async def async_step_local_api(self, user_input: dict[str, Any] | None = None) -> FlowResult: errors = {} if user_input is not None: @@ -117,10 +138,16 @@ def get_device_info(): return requests.get(f"{url}/info", timeout=10) response = await self.hass.async_add_executor_job(get_device_info) - + response.raise_for_status() response_json = response.json() - - await self.async_set_unique_id(response_json["serial_number"]) + except Exception: + errors["base"] = "cannot_connect" + else: + entry = await self.async_set_unique_id(response_json["serial_number"]) + if not entry or (CONF_ORIGINAL_DEVICE_NAME not in entry.data): + self._data[CONF_ORIGINAL_DEVICE_NAME] = response_json["device"]["name"] + + self._abort_if_unique_id_configured() return self.async_create_entry( title=response_json["device"]["name"], @@ -128,9 +155,6 @@ def get_device_info(): options={CONF_DEFAULT_NOTIFICATION_TITLE: ATTR_TITLE_DEFAULT}, ) - except Exception: - errors["base"] = "cannot_connect" - return self.async_show_form( step_id="local_api", data_schema=vol.Schema( @@ -144,14 +168,10 @@ def get_device_info(): errors=errors, ) - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: return await self.async_step_local_api() - async def async_step_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_confirm(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Confirm the setup.""" if user_input is not None: diff --git a/custom_components/hass_agent/const.py b/custom_components/hass_agent/const.py index 10bb4c1..b82f094 100644 --- a/custom_components/hass_agent/const.py +++ b/custom_components/hass_agent/const.py @@ -5,3 +5,4 @@ CONF_ACTION = "action" CONF_DEVICE_NAME = "device_name" CONF_DEFAULT_NOTIFICATION_TITLE = "default_notification_title" +CONF_ORIGINAL_DEVICE_NAME = "original_device_name" diff --git a/custom_components/hass_agent/manifest.json b/custom_components/hass_agent/manifest.json index c7bc04d..32c9bc7 100644 --- a/custom_components/hass_agent/manifest.json +++ b/custom_components/hass_agent/manifest.json @@ -1,23 +1,23 @@ { "domain": "hass_agent", "name": "HASS.Agent", - "version": "2.1.1", - "config_flow": true, - "documentation": "https://github.com/hass-agent/HASS.Agent-Integration", - "issue_tracker": "https://github.com/hass-agent/HASS.Agent-Integration/issues", - "mqtt": ["hass.agent/devices/#"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [ - "mqtt", - "http" - ], "codeowners": [ "@fillefilip8", "@DrR0X-glitch", "@amadeo-alex" ], + "config_flow": true, + "dependencies": [ + "mqtt", + "http" + ], + "documentation": "https://github.com/hass-agent/HASS.Agent-Integration", + "homekit": {}, "iot_class": "local_push", - "loggers": ["custom_components.hass_agent"] + "issue_tracker": "https://github.com/hass-agent/HASS.Agent-Integration/issues", + "loggers": ["custom_components.hass_agent"], + "mqtt": ["hass.agent/devices/#"], + "ssdp": [], + "version": "2.1.2", + "zeroconf": [] } diff --git a/custom_components/hass_agent/media_player.py b/custom_components/hass_agent/media_player.py index e0938de..f9c3a56 100644 --- a/custom_components/hass_agent/media_player.py +++ b/custom_components/hass_agent/media_player.py @@ -9,7 +9,7 @@ from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DOMAIN, CONF_ORIGINAL_DEVICE_NAME from homeassistant.components.mqtt.subscription import ( @@ -60,18 +60,16 @@ ) -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> bool: device_registry = dr.async_get(hass) device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) if device is None: return False - async_add_entities( - [HassAgentMediaPlayerDevice(entry.unique_id, entry.entry_id, device)] - ) + original_device_name = entry.data.get(CONF_ORIGINAL_DEVICE_NAME, device.name) + + async_add_entities([HassAgentMediaPlayerDevice(entry.unique_id, entry.entry_id, device, original_device_name)]) return True @@ -83,9 +81,7 @@ class HassAgentMediaPlayerDevice(MediaPlayerEntity): def update_thumbnail(self, message: ReceiveMessage): self.hass.data[DOMAIN][self._entry_id]["thumbnail"] = message.payload - self._attr_media_image_url = ( - f"/api/hass_agent/{self.entity_id}/thumbnail.png?time={time.time()}" - ) + self._attr_media_image_url = f"/api/hass_agent/{self.entity_id}/thumbnail.png?time={time.time()}" @property def media_image_local(self) -> str | None: @@ -94,6 +90,10 @@ def media_image_local(self) -> str | None: @callback def updated(self, message: ReceiveMessage): """Updates the media player with new data from MQTT""" + if not message.payload: + _logger.debug("received empty update message on '%s', ignoring", message.topic) + return + payload = json.loads(message.payload) self._state = payload["state"].lower() @@ -143,7 +143,7 @@ async def async_will_remove_from_hass(self) -> None: if self._listeners is not None: async_unsubscribe_topics(self.hass, self._listeners) - def __init__(self, unique_id, entry_id, device: dr.DeviceEntry): + def __init__(self, unique_id, entry_id, device: dr.DeviceEntry, original_device_name): """Initialize""" self._entry_id = entry_id self._name = device.name @@ -164,6 +164,7 @@ def __init__(self, unique_id, entry_id, device: dr.DeviceEntry): self._listeners = {} self._last_updated = 0 + self._original_device_name = original_device_name async def _send_command(self, command, data=None): """Send a command""" @@ -208,7 +209,7 @@ def available(self): def volume_level(self): """Return the volume level of the media player (0..1)""" return self._volume_level / 100.0 - + async def async_set_volume_level(self, volume: float) -> None: """Send new volume_level to device.""" volume = round(volume * 100) @@ -274,9 +275,7 @@ async def async_media_previous_track(self): """Send previous track command""" await self._send_command("previous") - async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None - ) -> BrowseMedia: + async def async_browse_media(self, media_content_type: str | None = None, media_content_id: str | None = None) -> BrowseMedia: """Implement the websocket media browsing helper.""" # If your media player has no own media sources to browse, route all browse commands # to the media source integration. diff --git a/custom_components/hass_agent/notify.py b/custom_components/hass_agent/notify.py index d084b9a..f1e8656 100644 --- a/custom_components/hass_agent/notify.py +++ b/custom_components/hass_agent/notify.py @@ -24,7 +24,10 @@ CONF_URL, ) -from .const import CONF_DEFAULT_NOTIFICATION_TITLE +from .const import ( + CONF_DEFAULT_NOTIFICATION_TITLE, + CONF_DEVICE_NAME, +) _logger = logging.getLogger(__name__) @@ -36,16 +39,21 @@ def get_service(hass, config, discovery_info=None): entry_id = discovery_info.get(CONF_ID, None) - return HassAgentNotificationService(hass, discovery_info[CONF_NAME], entry_id) + return HassAgentNotificationService( + hass, + discovery_info[CONF_DEVICE_NAME], + discovery_info[CONF_NAME], + entry_id, + ) class HassAgentNotificationService(BaseNotificationService): """Implementation of the HASS Agent notification service""" - def __init__(self, hass, name, entry_id): + def __init__(self, hass, device_name, service_name, entry_id): """Initialize the service.""" - self._service_name = name - self._device_name = name + self._service_name = service_name + self._device_name = device_name self._entry_id = entry_id self._hass = hass @@ -83,9 +91,7 @@ async def async_send_message(self, message: str, **kwargs: Any): elif media_source.is_media_source_id(image): sourced_media = await media_source.async_resolve_media(self.hass, image) - sourced_media = media_source.async_process_play_media_url( - self.hass, sourced_media.url - ) + sourced_media = media_source.async_process_play_media_url(self.hass, sourced_media.url) new_url = sourced_media if new_url is not None: @@ -110,9 +116,7 @@ def send_request(url, data): """Sends the json request""" return requests.post(url, json=data, timeout=10) - response = await self.hass.async_add_executor_job( - send_request, f"{entry.data[CONF_URL]}/notify", payload - ) + response = await self.hass.async_add_executor_job(send_request, f"{entry.data[CONF_URL]}/notify", payload) _logger.debug("Checking result") @@ -169,8 +173,6 @@ def send_request(url, data): response.reason, ) else: - _logger.debug( - "Unknown response %d: %s", response.status_code, response.reason - ) + _logger.debug("Unknown response %d: %s", response.status_code, response.reason) except Exception as ex: _logger.debug("Error sending message: %s", ex) diff --git a/custom_components/hass_agent/repairs.py b/custom_components/hass_agent/repairs.py new file mode 100644 index 0000000..f8cc607 --- /dev/null +++ b/custom_components/hass_agent/repairs.py @@ -0,0 +1,45 @@ +"""Repairs platform for HASS.Agent.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant +import voluptuous as vol + +from .const import DOMAIN, CONF_DEVICE_NAME + + +class RestartRequiredFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, issue_id: str, device_name: str) -> None: + self.issue_id = issue_id + self._device_name = device_name + + async def async_step_init(self, user_input: dict[str, str] | None = None) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await self.async_step_confirm_restart() + + async def async_step_confirm_restart(self, user_input: dict[str, str] | None = None) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + await self.hass.services.async_call("homeassistant", "restart") + return self.async_create_entry(title="", data={}) + + return self.async_show_form( + step_id="confirm_restart", + data_schema=vol.Schema({}), + description_placeholders={"name": self._device_name}, + ) + + +async def async_create_fix_flow(hass: HomeAssistant, issue_id: str, data: dict[str, str | int | float | None] | None = None) -> RepairsFlow | None: + """Create flow.""" + if issue_id.startswith("restart_required") and (data is not None): + return RestartRequiredFixFlow(issue_id, str(data[CONF_DEVICE_NAME])) + + return None diff --git a/custom_components/hass_agent/strings.json b/custom_components/hass_agent/strings.json index 6900a33..948ff7d 100644 --- a/custom_components/hass_agent/strings.json +++ b/custom_components/hass_agent/strings.json @@ -1,43 +1,57 @@ { - "config": { - "flow_title": "{name}", - "step": { - "confirm": { - "title": "[%key:common::config_flow::title::confirm%]", - "description": "[%key:common::config_flow::description::confirm%]" - }, - "local_api": { - "title": "[%key:common::config_flow::title::local_api%]", - "description": "[%key:common::config_flow::description::local_api%]", - "data": { - "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "ssl": "[%key:common::config_flow::data::ssl%]" + "config": { + "flow_title": "{name}", + "step": { + "confirm": { + "title": "[%key:common::config_flow::title::confirm%]", + "description": "[%key:common::config_flow::description::confirm%]" + }, + "local_api": { + "title": "[%key:common::config_flow::title::local_api%]", + "description": "[%key:common::config_flow::description::local_api%]", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "options": { + "step": { + "init": { + "data": { + "default_notification_title": "[%key:common::options::init::default_notification_title%]" + }, + "title": "[%key:common::options::init::title%]" + } + } }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" - } - }, - "options": { - "step": { - "init": { - "data": { - "default_notification_title": "[%key:common::options::init::default_notification_title%]" - }, - "title": "[%key:common::options::init::title%]" + "device_automation": { + "trigger_type": { + "notifications_mqtt": "[%key:component::hass_agent::device_automation::trigger_type::notifications_mqtt%]", + "notifications_event": "[%key:component::hass_agent::device_automation::trigger_type::notifications_event%]" + } + }, + "issues": { + "restart_required": { + "title": "[%key:component::hass_agent::issues::restart_required::title%]", + "fix_flow": { + "step": { + "confirm_restart": { + "title": "[%key:component::hass_agent::issues::restart_required::fix_flow::confirm_restart::title%]", + "description": "[%key:component::hass_agent::issues::restart_required::fix_flow::confirm_restart::description%]" + } + } + } } } - }, - "device_automation": { - "trigger_type": { - "notifications_mqtt": "When a notification with a specfic action has been pressed (MQTT)", - "notifications_event": "When a notification with a specfic action has been pressed (Event)" - } - } } diff --git a/custom_components/hass_agent/translations/en.json b/custom_components/hass_agent/translations/en.json index ad59414..4c620b2 100644 --- a/custom_components/hass_agent/translations/en.json +++ b/custom_components/hass_agent/translations/en.json @@ -2,10 +2,11 @@ "config": { "flow_title": "{name}", "abort": { - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network.", + "already_configured": "This device is already configured." }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect to HASS.Agent" }, "step": { "confirm": { @@ -34,8 +35,21 @@ }, "device_automation": { "trigger_type": { - "notifications_mqtt": "When a notification with a specfic action has been pressed (MQTT)", - "notifications_event": "When a notification with a specfic action has been pressed (Event)" + "notifications_mqtt": "When a notification with a specific action has been pressed (MQTT)", + "notifications_event": "When a notification with a specific action has been pressed (Event)" + } + }, + "issues": { + "restart_required": { + "title": "Device renamed - restart required", + "fix_flow": { + "step": { + "confirm_restart": { + "title": "Restart required", + "description": "Device name has been changed, restart of Home Assistant is required to properly reload notify service for {name}, click submit to restart now." + } + } + } } } }