Replace Guardian disable_ap
and enable_ap
services with a switch (#75034)
* Starter buttons * Ready to go * Replace Guardian `disable_ap` and `enable_ap` services with a switch * Clean up how actions are stored * Make similar to buttons * Remove service definitions * Docstring * Docstring * flake8 * Add repairs item * Add a repairs issue to notify of removed entity * Add entity replacement strategy * Fix repairs import * Update deprecation version * Remove breaking change * Include future breaking change version * Naming
This commit is contained in:
parent
1f410e884a
commit
13d3f4c3b2
7 changed files with 220 additions and 50 deletions
|
@ -238,11 +238,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
@call_with_data
|
||||
async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None:
|
||||
"""Disable the onboard AP."""
|
||||
async_log_deprecated_service_call(
|
||||
hass,
|
||||
call,
|
||||
"switch.turn_off",
|
||||
f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap",
|
||||
"2022.12.0",
|
||||
)
|
||||
await data.client.wifi.disable_ap()
|
||||
|
||||
@call_with_data
|
||||
async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None:
|
||||
"""Enable the onboard AP."""
|
||||
async_log_deprecated_service_call(
|
||||
hass,
|
||||
call,
|
||||
"switch.turn_on",
|
||||
f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap",
|
||||
"2022.12.0",
|
||||
)
|
||||
await data.client.wifi.enable_ap()
|
||||
|
||||
@call_with_data
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
|
@ -27,7 +28,11 @@ from .const import (
|
|||
DOMAIN,
|
||||
SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED,
|
||||
)
|
||||
from .util import GuardianDataUpdateCoordinator
|
||||
from .util import (
|
||||
EntityDomainReplacementStrategy,
|
||||
GuardianDataUpdateCoordinator,
|
||||
async_finish_entity_domain_replacements,
|
||||
)
|
||||
|
||||
ATTR_CONNECTED_CLIENTS = "connected_clients"
|
||||
|
||||
|
@ -79,6 +84,21 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up Guardian switches based on a config entry."""
|
||||
data: GuardianData = hass.data[DOMAIN][entry.entry_id]
|
||||
uid = entry.data[CONF_UID]
|
||||
|
||||
async_finish_entity_domain_replacements(
|
||||
hass,
|
||||
entry,
|
||||
(
|
||||
EntityDomainReplacementStrategy(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
f"{uid}_ap_enabled",
|
||||
f"switch.guardian_valve_controller_{uid}_onboard_ap",
|
||||
"2022.12.0",
|
||||
remove_old_entity=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@callback
|
||||
def add_new_paired_sensor(uid: str) -> None:
|
||||
|
|
|
@ -1,26 +1,4 @@
|
|||
# Describes the format for available Elexa Guardians services
|
||||
disable_ap:
|
||||
name: Disable AP
|
||||
description: Disable the device's onboard access point.
|
||||
fields:
|
||||
device_id:
|
||||
name: Valve Controller
|
||||
description: The valve controller whose AP should be disabled
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: guardian
|
||||
enable_ap:
|
||||
name: Enable AP
|
||||
description: Enable the device's onboard access point.
|
||||
fields:
|
||||
device_id:
|
||||
name: Valve Controller
|
||||
description: The valve controller whose AP should be enabled
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: guardian
|
||||
pair_sensor:
|
||||
name: Pair Sensor
|
||||
description: Add a new paired sensor to the valve controller.
|
||||
|
@ -39,6 +17,28 @@ pair_sensor:
|
|||
example: 5410EC688BCF
|
||||
selector:
|
||||
text:
|
||||
reboot:
|
||||
name: Reboot
|
||||
description: Reboot the device.
|
||||
fields:
|
||||
device_id:
|
||||
name: Valve Controller
|
||||
description: The valve controller to reboot
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: guardian
|
||||
reset_valve_diagnostics:
|
||||
name: Reset Valve Diagnostics
|
||||
description: Fully (and irrecoverably) reset all valve diagnostics.
|
||||
fields:
|
||||
device_id:
|
||||
name: Valve Controller
|
||||
description: The valve controller whose diagnostics should be reset
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: guardian
|
||||
unpair_sensor:
|
||||
name: Unpair Sensor
|
||||
description: Remove a paired sensor from the valve controller.
|
||||
|
|
|
@ -25,7 +25,18 @@
|
|||
"step": {
|
||||
"confirm": {
|
||||
"title": "The {deprecated_service} service is being removed",
|
||||
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved."
|
||||
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"replaced_old_entity": {
|
||||
"title": "The {old_entity_id} entity will be removed",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "The {old_entity_id} entity will be removed",
|
||||
"description": "This entity has been replaced by `{replacement_entity_id}`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,86 @@
|
|||
"""Switches for the Elexa Guardian integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aioguardian import Client
|
||||
from aioguardian.errors import GuardianError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription
|
||||
from .const import API_VALVE_STATUS, DOMAIN
|
||||
from .const import API_VALVE_STATUS, API_WIFI_STATUS, DOMAIN
|
||||
|
||||
ATTR_AVG_CURRENT = "average_current"
|
||||
ATTR_CONNECTED_CLIENTS = "connected_clients"
|
||||
ATTR_INST_CURRENT = "instantaneous_current"
|
||||
ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt"
|
||||
ATTR_STATION_CONNECTED = "station_connected"
|
||||
ATTR_TRAVEL_COUNT = "travel_count"
|
||||
|
||||
SWITCH_KIND_ONBOARD_AP = "onboard_ap"
|
||||
SWITCH_KIND_VALVE = "valve"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SwitchDescriptionMixin:
|
||||
"""Define an entity description mixin for Guardian switches."""
|
||||
|
||||
off_action: Callable[[Client], Awaitable]
|
||||
on_action: Callable[[Client], Awaitable]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValveControllerSwitchDescription(
|
||||
SwitchEntityDescription, ValveControllerEntityDescription
|
||||
SwitchEntityDescription, ValveControllerEntityDescription, SwitchDescriptionMixin
|
||||
):
|
||||
"""Describe a Guardian valve controller switch."""
|
||||
|
||||
|
||||
async def _async_disable_ap(client: Client) -> None:
|
||||
"""Disable the onboard AP."""
|
||||
await client.wifi.disable_ap()
|
||||
|
||||
|
||||
async def _async_enable_ap(client: Client) -> None:
|
||||
"""Enable the onboard AP."""
|
||||
await client.wifi.enable_ap()
|
||||
|
||||
|
||||
async def _async_close_valve(client: Client) -> None:
|
||||
"""Close the valve."""
|
||||
await client.valve.close()
|
||||
|
||||
|
||||
async def _async_open_valve(client: Client) -> None:
|
||||
"""Open the valve."""
|
||||
await client.valve.open()
|
||||
|
||||
|
||||
VALVE_CONTROLLER_DESCRIPTIONS = (
|
||||
ValveControllerSwitchDescription(
|
||||
key=SWITCH_KIND_ONBOARD_AP,
|
||||
name="Onboard AP",
|
||||
icon="mdi:wifi",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
api_category=API_WIFI_STATUS,
|
||||
off_action=_async_disable_ap,
|
||||
on_action=_async_enable_ap,
|
||||
),
|
||||
ValveControllerSwitchDescription(
|
||||
key=SWITCH_KIND_VALVE,
|
||||
name="Valve controller",
|
||||
icon="mdi:water",
|
||||
api_category=API_VALVE_STATUS,
|
||||
off_action=_async_close_valve,
|
||||
on_action=_async_open_valve,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -53,9 +98,7 @@ async def async_setup_entry(
|
|||
|
||||
|
||||
class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
||||
"""Define a switch to open/close the Guardian valve."""
|
||||
|
||||
entity_description: ValveControllerSwitchDescription
|
||||
"""Define a switch related to a Guardian valve controller."""
|
||||
|
||||
ON_STATES = {
|
||||
"start_opening",
|
||||
|
@ -64,6 +107,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
|||
"opened",
|
||||
}
|
||||
|
||||
entity_description: ValveControllerSwitchDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
|
@ -73,12 +118,20 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
|||
"""Initialize."""
|
||||
super().__init__(entry, data.valve_controller_coordinators, description)
|
||||
|
||||
self._attr_is_on = True
|
||||
self._client = data.client
|
||||
|
||||
@callback
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity."""
|
||||
if self.entity_description.key == SWITCH_KIND_ONBOARD_AP:
|
||||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
ATTR_CONNECTED_CLIENTS: self.coordinator.data.get("ap_clients"),
|
||||
ATTR_STATION_CONNECTED: self.coordinator.data["station_connected"],
|
||||
}
|
||||
)
|
||||
self._attr_is_on = self.coordinator.data["ap_enabled"]
|
||||
elif self.entity_description.key == SWITCH_KIND_VALVE:
|
||||
self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES
|
||||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
|
@ -92,23 +145,27 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
|||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the valve off (closed)."""
|
||||
"""Turn the switch off."""
|
||||
try:
|
||||
async with self._client:
|
||||
await self._client.valve.close()
|
||||
await self.entity_description.off_action(self._client)
|
||||
except GuardianError as err:
|
||||
raise HomeAssistantError(f"Error while closing the valve: {err}") from err
|
||||
raise HomeAssistantError(
|
||||
f'Error while turning "{self.entity_id}" off: {err}'
|
||||
) from err
|
||||
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the valve on (open)."""
|
||||
"""Turn the switch on."""
|
||||
try:
|
||||
async with self._client:
|
||||
await self._client.valve.open()
|
||||
await self.entity_description.on_action(self._client)
|
||||
except GuardianError as err:
|
||||
raise HomeAssistantError(f"Error while opening the valve: {err}") from err
|
||||
raise HomeAssistantError(
|
||||
f'Error while turning "{self.entity_id}" on: {err}'
|
||||
) from err
|
||||
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -23,12 +23,23 @@
|
|||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved.",
|
||||
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.",
|
||||
"title": "The {deprecated_service} service is being removed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "The {deprecated_service} service is being removed"
|
||||
},
|
||||
"replaced_old_entity": {
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "This entity has been replaced by `{replacement_entity_id}`.",
|
||||
"title": "The {old_entity_id} entity will be removed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "The {old_entity_id} entity will be removed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
|
@ -11,16 +12,72 @@ from aioguardian.errors import GuardianError
|
|||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityDomainReplacementStrategy:
|
||||
"""Define an entity replacement."""
|
||||
|
||||
old_domain: str
|
||||
old_unique_id: str
|
||||
replacement_entity_id: str
|
||||
breaks_in_ha_version: str
|
||||
remove_old_entity: bool = True
|
||||
|
||||
|
||||
@callback
|
||||
def async_finish_entity_domain_replacements(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy],
|
||||
) -> None:
|
||||
"""Remove old entities and create a repairs issue with info on their replacement."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
for strategy in entity_replacement_strategies:
|
||||
try:
|
||||
[registry_entry] = [
|
||||
registry_entry
|
||||
for registry_entry in ent_reg.entities.values()
|
||||
if registry_entry.config_entry_id == entry.entry_id
|
||||
and registry_entry.domain == strategy.old_domain
|
||||
and registry_entry.unique_id == strategy.old_unique_id
|
||||
]
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
old_entity_id = registry_entry.entity_id
|
||||
translation_key = "replaced_old_entity"
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{translation_key}_{old_entity_id}",
|
||||
breaks_in_ha_version=strategy.breaks_in_ha_version,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
"old_entity_id": old_entity_id,
|
||||
"replacement_entity_id": strategy.replacement_entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
if strategy.remove_old_entity:
|
||||
LOGGER.info('Removing old entity: "%s"', old_entity_id)
|
||||
ent_reg.async_remove(old_entity_id)
|
||||
|
||||
|
||||
class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]):
|
||||
"""Define an extended DataUpdateCoordinator with some Guardian goodies."""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue