mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Alter RainMachine to enable/disable program/zones via separate switches (#59617)
This commit is contained in:
parent
4ff3b2e9a9
commit
0e4de42539
@ -21,8 +21,12 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
from homeassistant.helpers import (
|
||||||
import homeassistant.helpers.device_registry as dr
|
aiohttp_client,
|
||||||
|
config_validation as cv,
|
||||||
|
device_registry as dr,
|
||||||
|
entity_registry as er,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
@ -323,6 +327,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Migrate an old config entry."""
|
||||||
|
version = entry.version
|
||||||
|
|
||||||
|
LOGGER.debug("Migrating from version %s", version)
|
||||||
|
|
||||||
|
# 1 -> 2: Update unique IDs to be consistent across platform (including removing
|
||||||
|
# the silly removal of colons in the MAC address that was added originally):
|
||||||
|
if version == 1:
|
||||||
|
version = entry.version = 2
|
||||||
|
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
for entity_entry in [
|
||||||
|
e for e in ent_reg.entities.values() if e.config_entry_id == entry.entry_id
|
||||||
|
]:
|
||||||
|
unique_id_pieces = entity_entry.unique_id.split("_")
|
||||||
|
old_mac = unique_id_pieces[0]
|
||||||
|
new_mac = ":".join(old_mac[i : i + 2] for i in range(0, len(old_mac), 2))
|
||||||
|
unique_id_pieces[0] = new_mac
|
||||||
|
|
||||||
|
if entity_entry.entity_id.startswith("switch"):
|
||||||
|
unique_id_pieces[1] = unique_id_pieces[1][11:].lower()
|
||||||
|
|
||||||
|
ent_reg.async_update_entity(
|
||||||
|
entity_entry.entity_id, new_unique_id="_".join(unique_id_pieces)
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER.info("Migration to version %s successful", version)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Handle an options update."""
|
"""Handle an options update."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
@ -355,10 +391,7 @@ class RainMachineEntity(CoordinatorEntity):
|
|||||||
)
|
)
|
||||||
self._attr_extra_state_attributes = {}
|
self._attr_extra_state_attributes = {}
|
||||||
self._attr_name = f"{controller.name} {description.name}"
|
self._attr_name = f"{controller.name} {description.name}"
|
||||||
# The colons are removed from the device MAC simply because that value
|
self._attr_unique_id = f"{controller.mac}_{description.key}"
|
||||||
# (unnecessarily) makes up the existing unique ID formula and we want to avoid
|
|
||||||
# a breaking change:
|
|
||||||
self._attr_unique_id = f"{controller.mac.replace(':', '')}_{description.key}"
|
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ async def async_get_controller(
|
|||||||
class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a RainMachine config flow."""
|
"""Handle a RainMachine config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
|
|
||||||
discovered_ip_address: str | None = None
|
discovered_ip_address: str | None = None
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""This component provides support for RainMachine programs and zones."""
|
"""This component provides support for RainMachine programs and zones."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -12,8 +13,9 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ID
|
from homeassistant.const import ATTR_ID, ENTITY_CATEGORY_CONFIG
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
@ -51,8 +53,6 @@ ATTR_TIME_REMAINING = "time_remaining"
|
|||||||
ATTR_VEGETATION_TYPE = "vegetation_type"
|
ATTR_VEGETATION_TYPE = "vegetation_type"
|
||||||
ATTR_ZONES = "zones"
|
ATTR_ZONES = "zones"
|
||||||
|
|
||||||
DEFAULT_ICON = "mdi:water"
|
|
||||||
|
|
||||||
DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
|
||||||
RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"}
|
RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"}
|
||||||
@ -130,10 +130,6 @@ async def async_setup_entry(
|
|||||||
platform = entity_platform.async_get_current_platform()
|
platform = entity_platform.async_get_current_platform()
|
||||||
|
|
||||||
for service_name, schema, method in (
|
for service_name, schema, method in (
|
||||||
("disable_program", {}, "async_disable_program"),
|
|
||||||
("disable_zone", {}, "async_disable_zone"),
|
|
||||||
("enable_program", {}, "async_enable_program"),
|
|
||||||
("enable_zone", {}, "async_enable_zone"),
|
|
||||||
("start_program", {}, "async_start_program"),
|
("start_program", {}, "async_start_program"),
|
||||||
(
|
(
|
||||||
"start_zone",
|
"start_zone",
|
||||||
@ -149,44 +145,55 @@ async def async_setup_entry(
|
|||||||
):
|
):
|
||||||
platform.async_register_entity_service(service_name, schema, method)
|
platform.async_register_entity_service(service_name, schema, method)
|
||||||
|
|
||||||
controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER]
|
data = hass.data[DOMAIN][entry.entry_id]
|
||||||
programs_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][
|
controller = data[DATA_CONTROLLER]
|
||||||
DATA_PROGRAMS
|
program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS]
|
||||||
]
|
zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES]
|
||||||
zones_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][DATA_ZONES]
|
|
||||||
|
|
||||||
entities: list[RainMachineProgram | RainMachineZone] = [
|
entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = []
|
||||||
RainMachineProgram(
|
|
||||||
|
for kind, coordinator, switch_class, switch_enabled_class in (
|
||||||
|
("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled),
|
||||||
|
("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled),
|
||||||
|
):
|
||||||
|
for uid, data in coordinator.data.items():
|
||||||
|
# Add a switch to start/stop the program or zone:
|
||||||
|
entities.append(
|
||||||
|
switch_class(
|
||||||
entry,
|
entry,
|
||||||
programs_coordinator,
|
coordinator,
|
||||||
controller,
|
controller,
|
||||||
RainMachineSwitchDescription(
|
RainMachineSwitchDescription(
|
||||||
key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid
|
key=f"{kind}_{uid}",
|
||||||
|
name=data["name"],
|
||||||
|
icon="mdi:water",
|
||||||
|
uid=uid,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for uid, program in programs_coordinator.data.items()
|
)
|
||||||
]
|
|
||||||
entities.extend(
|
# Add a switch to enabled/disable the program or zone:
|
||||||
[
|
entities.append(
|
||||||
RainMachineZone(
|
switch_enabled_class(
|
||||||
entry,
|
entry,
|
||||||
zones_coordinator,
|
coordinator,
|
||||||
controller,
|
controller,
|
||||||
RainMachineSwitchDescription(
|
RainMachineSwitchDescription(
|
||||||
key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid
|
key=f"{kind}_{uid}_enabled",
|
||||||
|
name=f"{data['name']} Enabled",
|
||||||
|
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||||
|
icon="mdi:cog",
|
||||||
|
uid=uid,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for uid, zone in zones_coordinator.data.items()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity):
|
||||||
"""A class to represent a generic RainMachine switch."""
|
"""Define a base RainMachine switch."""
|
||||||
|
|
||||||
_attr_icon = DEFAULT_ICON
|
|
||||||
entity_description: RainMachineSwitchDescription
|
entity_description: RainMachineSwitchDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -196,37 +203,30 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
|||||||
controller: Controller,
|
controller: Controller,
|
||||||
description: RainMachineSwitchDescription,
|
description: RainMachineSwitchDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a generic RainMachine switch."""
|
"""Initialize."""
|
||||||
super().__init__(entry, coordinator, controller, description)
|
super().__init__(entry, coordinator, controller, description)
|
||||||
|
|
||||||
self._attr_is_on = False
|
self._attr_is_on = False
|
||||||
self._data = coordinator.data[self.entity_description.uid]
|
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
self._is_active = True
|
|
||||||
|
|
||||||
@property
|
async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None:
|
||||||
def available(self) -> bool:
|
"""Await an API coroutine, handle any errors, and update as appropriate."""
|
||||||
"""Return True if entity is available."""
|
|
||||||
return super().available and self._is_active
|
|
||||||
|
|
||||||
async def _async_run_switch_coroutine(self, api_coro: Coroutine) -> None:
|
|
||||||
"""Run a coroutine to toggle the switch."""
|
|
||||||
try:
|
try:
|
||||||
resp = await api_coro
|
resp = await api_coro
|
||||||
except RequestError as err:
|
except RequestError as err:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Error while toggling %s "%s": %s',
|
'Error while executing %s on "%s": %s',
|
||||||
self.entity_description.key,
|
api_coro.__name__,
|
||||||
self.unique_id,
|
self.name,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if resp["statusCode"] != 0:
|
if resp["statusCode"] != 0:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Error while toggling %s "%s": %s',
|
'Error while executing %s on "%s": %s',
|
||||||
self.entity_description.key,
|
api_coro.__name__,
|
||||||
self.unique_id,
|
self.name,
|
||||||
resp["message"],
|
resp["message"],
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -237,94 +237,102 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
|
|||||||
async_update_programs_and_zones(self.hass, self._entry)
|
async_update_programs_and_zones(self.hass, self._entry)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_disable_program(self) -> None:
|
|
||||||
"""Disable a program."""
|
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
|
||||||
|
|
||||||
async def async_disable_zone(self) -> None:
|
|
||||||
"""Disable a zone."""
|
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
|
||||||
|
|
||||||
async def async_enable_program(self) -> None:
|
|
||||||
"""Enable a program."""
|
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
|
||||||
|
|
||||||
async def async_enable_zone(self) -> None:
|
|
||||||
"""Enable a zone."""
|
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
|
||||||
|
|
||||||
async def async_start_program(self) -> None:
|
async def async_start_program(self) -> None:
|
||||||
"""Start a program."""
|
"""Execute the start_program entity service."""
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
raise NotImplementedError("Service not implemented for this entity")
|
||||||
|
|
||||||
async def async_start_zone(self, *, zone_run_time: int) -> None:
|
async def async_start_zone(self, *, zone_run_time: int) -> None:
|
||||||
"""Start a zone."""
|
"""Execute the start_zone entity service."""
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
raise NotImplementedError("Service not implemented for this entity")
|
||||||
|
|
||||||
async def async_stop_program(self) -> None:
|
async def async_stop_program(self) -> None:
|
||||||
"""Stop a program."""
|
"""Execute the stop_program entity service."""
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
raise NotImplementedError("Service not implemented for this entity")
|
||||||
|
|
||||||
async def async_stop_zone(self) -> None:
|
async def async_stop_zone(self) -> None:
|
||||||
"""Stop a zone."""
|
"""Execute the stop_zone entity service."""
|
||||||
raise NotImplementedError("Service not implemented for this entity")
|
raise NotImplementedError("Service not implemented for this entity")
|
||||||
|
|
||||||
|
|
||||||
|
class RainMachineActivitySwitch(RainMachineBaseSwitch):
|
||||||
|
"""Define a RainMachine switch to start/stop an activity (program or zone)."""
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off.
|
||||||
|
|
||||||
|
The only way this could occur is if someone rapidly turns a disabled activity
|
||||||
|
off right after turning it on.
|
||||||
|
"""
|
||||||
|
if not self.coordinator.data[self.entity_description.uid]["active"]:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Cannot turn off an inactive program/zone: {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.async_turn_off_when_active(**kwargs)
|
||||||
|
|
||||||
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off when its associated activity is active."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
if not self.coordinator.data[self.entity_description.uid]["active"]:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"Cannot turn on an inactive program/zone: {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.async_turn_on_when_active(**kwargs)
|
||||||
|
|
||||||
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on when its associated activity is active."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class RainMachineEnabledSwitch(RainMachineBaseSwitch):
|
||||||
|
"""Define a RainMachine switch to enable/disable an activity (program or zone)."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
"""Update the state."""
|
"""Update the entity when new data is received."""
|
||||||
self._data = self.coordinator.data[self.entity_description.uid]
|
self._attr_is_on = self.coordinator.data[self.entity_description.uid]["active"]
|
||||||
self._is_active = self._data["active"]
|
|
||||||
|
|
||||||
|
|
||||||
class RainMachineProgram(RainMachineSwitch):
|
class RainMachineProgram(RainMachineActivitySwitch):
|
||||||
"""A RainMachine program."""
|
"""Define a RainMachine program."""
|
||||||
|
|
||||||
@property
|
|
||||||
def zones(self) -> list:
|
|
||||||
"""Return a list of active zones associated with this program."""
|
|
||||||
return [z for z in self._data["wateringTimes"] if z["active"]]
|
|
||||||
|
|
||||||
async def async_disable_program(self) -> None:
|
|
||||||
"""Disable a program."""
|
|
||||||
await self._controller.programs.disable(self.entity_description.uid)
|
|
||||||
await async_update_programs_and_zones(self.hass, self._entry)
|
|
||||||
|
|
||||||
async def async_enable_program(self) -> None:
|
|
||||||
"""Enable a program."""
|
|
||||||
await self._controller.programs.enable(self.entity_description.uid)
|
|
||||||
await async_update_programs_and_zones(self.hass, self._entry)
|
|
||||||
|
|
||||||
async def async_start_program(self) -> None:
|
async def async_start_program(self) -> None:
|
||||||
"""Start a program."""
|
"""Start the program."""
|
||||||
await self.async_turn_on()
|
await self.async_turn_on()
|
||||||
|
|
||||||
async def async_stop_program(self) -> None:
|
async def async_stop_program(self) -> None:
|
||||||
"""Stop a program."""
|
"""Stop the program."""
|
||||||
await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the program off."""
|
"""Turn the switch off when its associated activity is active."""
|
||||||
await self._async_run_switch_coroutine(
|
await self._async_run_api_coroutine(
|
||||||
self._controller.programs.stop(self.entity_description.uid)
|
self._controller.programs.stop(self.entity_description.uid)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the program on."""
|
"""Turn the switch on when its associated activity is active."""
|
||||||
await self._async_run_switch_coroutine(
|
await self._async_run_api_coroutine(
|
||||||
self._controller.programs.start(self.entity_description.uid)
|
self._controller.programs.start(self.entity_description.uid)
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
"""Update the state."""
|
"""Update the entity when new data is received."""
|
||||||
super().update_from_latest_data()
|
data = self.coordinator.data[self.entity_description.uid]
|
||||||
|
|
||||||
self._attr_is_on = bool(self._data["status"])
|
self._attr_is_on = bool(data["status"])
|
||||||
|
|
||||||
next_run: str | None = None
|
next_run: str | None
|
||||||
if self._data.get("nextRun") is not None:
|
if data.get("nextRun") is None:
|
||||||
|
next_run = None
|
||||||
|
else:
|
||||||
next_run = datetime.strptime(
|
next_run = datetime.strptime(
|
||||||
f"{self._data['nextRun']} {self._data['startTime']}",
|
f"{data['nextRun']} {data['startTime']}",
|
||||||
"%Y-%m-%d %H:%M",
|
"%Y-%m-%d %H:%M",
|
||||||
).isoformat()
|
).isoformat()
|
||||||
|
|
||||||
@ -332,76 +340,107 @@ class RainMachineProgram(RainMachineSwitch):
|
|||||||
{
|
{
|
||||||
ATTR_ID: self.entity_description.uid,
|
ATTR_ID: self.entity_description.uid,
|
||||||
ATTR_NEXT_RUN: next_run,
|
ATTR_NEXT_RUN: next_run,
|
||||||
ATTR_SOAK: self.coordinator.data[self.entity_description.uid].get(
|
ATTR_SOAK: data.get("soak"),
|
||||||
"soak"
|
ATTR_STATUS: RUN_STATUS_MAP[data["status"]],
|
||||||
),
|
ATTR_ZONES: [z for z in data["wateringTimes"] if z["active"]],
|
||||||
ATTR_STATUS: RUN_STATUS_MAP[
|
|
||||||
self.coordinator.data[self.entity_description.uid]["status"]
|
|
||||||
],
|
|
||||||
ATTR_ZONES: ", ".join(z["name"] for z in self.zones),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RainMachineZone(RainMachineSwitch):
|
class RainMachineProgramEnabled(RainMachineEnabledSwitch):
|
||||||
"""A RainMachine zone."""
|
"""Define a switch to enable/disable a RainMachine program."""
|
||||||
|
|
||||||
async def async_disable_zone(self) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Disable a zone."""
|
"""Disable the program."""
|
||||||
await self._controller.zones.disable(self.entity_description.uid)
|
tasks = [
|
||||||
await async_update_programs_and_zones(self.hass, self._entry)
|
self._async_run_api_coroutine(
|
||||||
|
self._controller.programs.stop(self.entity_description.uid)
|
||||||
|
),
|
||||||
|
self._async_run_api_coroutine(
|
||||||
|
self._controller.programs.disable(self.entity_description.uid)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
async def async_enable_zone(self) -> None:
|
await asyncio.gather(*tasks)
|
||||||
"""Enable a zone."""
|
|
||||||
await self._controller.zones.enable(self.entity_description.uid)
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
await async_update_programs_and_zones(self.hass, self._entry)
|
"""Enable the program."""
|
||||||
|
await self._async_run_api_coroutine(
|
||||||
|
self._controller.programs.enable(self.entity_description.uid)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RainMachineZone(RainMachineActivitySwitch):
|
||||||
|
"""Define a RainMachine zone."""
|
||||||
|
|
||||||
async def async_start_zone(self, *, zone_run_time: int) -> None:
|
async def async_start_zone(self, *, zone_run_time: int) -> None:
|
||||||
"""Start a particular zone for a certain amount of time."""
|
"""Start a particular zone for a certain amount of time."""
|
||||||
await self._controller.zones.start(self.entity_description.uid, zone_run_time)
|
await self.async_turn_off(duration=zone_run_time)
|
||||||
await async_update_programs_and_zones(self.hass, self._entry)
|
|
||||||
|
|
||||||
async def async_stop_zone(self) -> None:
|
async def async_stop_zone(self) -> None:
|
||||||
"""Stop a zone."""
|
"""Stop a zone."""
|
||||||
await self.async_turn_off()
|
await self.async_turn_off()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the zone off."""
|
"""Turn the switch off when its associated activity is active."""
|
||||||
await self._async_run_switch_coroutine(
|
await self._async_run_api_coroutine(
|
||||||
self._controller.zones.stop(self.entity_description.uid)
|
self._controller.zones.stop(self.entity_description.uid)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on_when_active(self, **kwargs: Any) -> None:
|
||||||
"""Turn the zone on."""
|
"""Turn the switch on when its associated activity is active."""
|
||||||
await self._async_run_switch_coroutine(
|
await self._async_run_api_coroutine(
|
||||||
self._controller.zones.start(
|
self._controller.zones.start(
|
||||||
self.entity_description.uid,
|
self.entity_description.uid,
|
||||||
self._entry.options[CONF_ZONE_RUN_TIME],
|
kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def update_from_latest_data(self) -> None:
|
def update_from_latest_data(self) -> None:
|
||||||
"""Update the state."""
|
"""Update the entity when new data is received."""
|
||||||
super().update_from_latest_data()
|
data = self.coordinator.data[self.entity_description.uid]
|
||||||
|
|
||||||
self._attr_is_on = bool(self._data["state"])
|
self._attr_is_on = bool(data["state"])
|
||||||
|
|
||||||
self._attr_extra_state_attributes.update(
|
self._attr_extra_state_attributes.update(
|
||||||
{
|
{
|
||||||
ATTR_STATUS: RUN_STATUS_MAP[self._data["state"]],
|
ATTR_AREA: data.get("waterSense").get("area"),
|
||||||
ATTR_AREA: self._data.get("waterSense").get("area"),
|
ATTR_CURRENT_CYCLE: data.get("cycle"),
|
||||||
ATTR_CURRENT_CYCLE: self._data.get("cycle"),
|
ATTR_FIELD_CAPACITY: data.get("waterSense").get("fieldCapacity"),
|
||||||
ATTR_FIELD_CAPACITY: self._data.get("waterSense").get("fieldCapacity"),
|
ATTR_ID: data["uid"],
|
||||||
ATTR_ID: self._data["uid"],
|
ATTR_NO_CYCLES: data.get("noOfCycles"),
|
||||||
ATTR_NO_CYCLES: self._data.get("noOfCycles"),
|
ATTR_PRECIP_RATE: data.get("waterSense").get("precipitationRate"),
|
||||||
ATTR_PRECIP_RATE: self._data.get("waterSense").get("precipitationRate"),
|
ATTR_RESTRICTIONS: data.get("restriction"),
|
||||||
ATTR_RESTRICTIONS: self._data.get("restriction"),
|
ATTR_SLOPE: SLOPE_TYPE_MAP.get(data.get("slope")),
|
||||||
ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._data.get("slope")),
|
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data.get("soil")),
|
||||||
ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._data.get("soil")),
|
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data.get("group_id")),
|
||||||
ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(self._data.get("group_id")),
|
ATTR_STATUS: RUN_STATUS_MAP[data["state"]],
|
||||||
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(self._data.get("sun")),
|
ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")),
|
||||||
ATTR_TIME_REMAINING: self._data.get("remaining"),
|
ATTR_TIME_REMAINING: data.get("remaining"),
|
||||||
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._data.get("type")),
|
ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data.get("type")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RainMachineZoneEnabled(RainMachineEnabledSwitch):
|
||||||
|
"""Define a switch to enable/disable a RainMachine zone."""
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Disable the zone."""
|
||||||
|
tasks = [
|
||||||
|
self._async_run_api_coroutine(
|
||||||
|
self._controller.zones.stop(self.entity_description.uid)
|
||||||
|
),
|
||||||
|
self._async_run_api_coroutine(
|
||||||
|
self._controller.zones.disable(self.entity_description.uid)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Enable the zone."""
|
||||||
|
await self._async_run_api_coroutine(
|
||||||
|
self._controller.zones.enable(self.entity_description.uid)
|
||||||
|
)
|
||||||
|
@ -4,10 +4,11 @@ from unittest.mock import AsyncMock, Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
from regenmaschine.errors import RainMachineError
|
from regenmaschine.errors import RainMachineError
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN
|
from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -70,6 +71,68 @@ async def test_invalid_password(hass):
|
|||||||
assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
|
assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"platform,entity_name,entity_id,old_unique_id,new_unique_id",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"binary_sensor",
|
||||||
|
"Home Flow Sensor",
|
||||||
|
"binary_sensor.home_flow_sensor",
|
||||||
|
"60e32719b6cf_flow_sensor",
|
||||||
|
"60:e3:27:19:b6:cf_flow_sensor",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"switch",
|
||||||
|
"Home Landscaping",
|
||||||
|
"switch.home_landscaping",
|
||||||
|
"60e32719b6cf_RainMachineZone_1",
|
||||||
|
"60:e3:27:19:b6:cf_zone_1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_migrate_1_2(
|
||||||
|
hass, platform, entity_name, entity_id, old_unique_id, new_unique_id
|
||||||
|
):
|
||||||
|
"""Test migration from version 1 to 2 (consistent unique IDs)."""
|
||||||
|
conf = {
|
||||||
|
CONF_IP_ADDRESS: "192.168.1.100",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_PORT: 8080,
|
||||||
|
CONF_SSL: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
|
# Create entity RegistryEntry using old unique ID format:
|
||||||
|
entity_entry = ent_reg.async_get_or_create(
|
||||||
|
platform,
|
||||||
|
DOMAIN,
|
||||||
|
old_unique_id,
|
||||||
|
suggested_object_id=entity_name,
|
||||||
|
config_entry=entry,
|
||||||
|
original_name=entity_name,
|
||||||
|
)
|
||||||
|
assert entity_entry.entity_id == entity_id
|
||||||
|
assert entity_entry.unique_id == old_unique_id
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.rainmachine.async_setup_entry", return_value=True
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.rainmachine.config_flow.Client",
|
||||||
|
return_value=_get_mock_client(),
|
||||||
|
):
|
||||||
|
await setup.async_setup_component(hass, DOMAIN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check that new RegistryEntry is using new unique ID format
|
||||||
|
entity_entry = ent_reg.async_get(entity_id)
|
||||||
|
assert entity_entry.unique_id == new_unique_id
|
||||||
|
assert ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id) is None
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow(hass):
|
async def test_options_flow(hass):
|
||||||
"""Test config flow options."""
|
"""Test config flow options."""
|
||||||
conf = {
|
conf = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user