Add zones support to Shelly Irrigation controller (#152382)

This commit is contained in:
Shay Levy
2025-10-25 23:33:20 +03:00
committed by GitHub
parent 27516dee6a
commit 2125a4123d
4 changed files with 133 additions and 6 deletions

View File

@@ -165,6 +165,7 @@ RPC_SWITCHES = {
"boolean_zone0": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
@@ -175,6 +176,7 @@ RPC_SWITCHES = {
"boolean_zone1": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
@@ -185,6 +187,7 @@ RPC_SWITCHES = {
"boolean_zone2": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
@@ -195,6 +198,7 @@ RPC_SWITCHES = {
"boolean_zone3": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
@@ -205,6 +209,7 @@ RPC_SWITCHES = {
"boolean_zone4": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
@@ -215,6 +220,7 @@ RPC_SWITCHES = {
"boolean_zone5": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",

View File

@@ -417,6 +417,11 @@ def get_rpc_sub_device_name(
"""Get name based on device and channel name."""
if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if (zone_id := get_irrigation_zone_id(device.config, key)) is not None:
# workaround for Irrigation controller, name stored in "service:0"
if zone_name := device.config["service:0"]["zones"][zone_id]["name"]:
return cast(str, zone_name)
if entity_name := device.config[key].get("name"):
return cast(str, entity_name)
@@ -787,6 +792,13 @@ async def get_rpc_scripts_event_types(
return script_events
def get_irrigation_zone_id(config: dict[str, Any], key: str) -> int | None:
"""Return the zone id if the component is an irrigation zone."""
if key in config and (zone := get_rpc_role_by_key(config, key)).startswith("zone"):
return int(zone[4:])
return None
def get_rpc_device_info(
device: RpcDevice,
mac: str,
@@ -823,7 +835,10 @@ def get_rpc_device_info(
)
if (
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
(
component not in (*All_LIGHT_TYPES, "cover", "em1", "switch")
and get_irrigation_zone_id(device.config, key) is None
)
or idx is None
or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2
):

View File

@@ -17,7 +17,11 @@ from homeassistant.components.valve import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import MODEL_FRANKEVER_WATER_VALVE, MODEL_NEO_WATER_VALVE
from .const import (
MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
MODEL_FRANKEVER_WATER_VALVE,
MODEL_NEO_WATER_VALVE,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
@@ -92,8 +96,8 @@ class RpcShellyWaterValve(RpcShellyBaseWaterValve):
await self.coordinator.device.number_set(self._id, position)
class RpcShellyNeoWaterValve(RpcShellyBaseWaterValve):
"""Entity that controls a valve on RPC Shelly NEO Water Valve."""
class RpcShellySimpleWaterValve(RpcShellyBaseWaterValve):
"""Entity that controls a valve on RPC Shelly Open/Close Water Valve."""
_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
_attr_reports_position = False
@@ -124,9 +128,51 @@ RPC_VALVES: dict[str, RpcValveDescription] = {
key="boolean",
sub_key="value",
role="state",
entity_class=RpcShellyNeoWaterValve,
entity_class=RpcShellySimpleWaterValve,
models={MODEL_NEO_WATER_VALVE},
),
"boolean_zone0": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone0",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone1": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone1",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone2": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone2",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone3": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone3",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone4": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone4",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone5": RpcValveDescription(
key="boolean",
sub_key="value",
role="zone5",
entity_class=RpcShellySimpleWaterValve,
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
}

View File

@@ -12,7 +12,10 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.shelly.const import DOMAIN
from homeassistant.components.shelly.const import (
DOMAIN,
MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.entity_registry import EntityRegistry
@@ -475,6 +478,63 @@ async def test_shelly_pro_3em_with_emeter_name(
assert device_entry.name == "Test name Phase C"
async def test_shelly_fk_06x_with_zone_names(
hass: HomeAssistant,
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
device_registry: DeviceRegistry,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test Shelly Irrigation controller FK-06X with zone names.
We should get the main device and 6 subdevices, one subdevice per one zone.
"""
device_fixture = await async_load_json_object_fixture(
hass, "fk-06x_gen3_irrigation.json", DOMAIN
)
monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"])
monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"])
monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"])
await init_integration(hass, gen=3, model=MODEL_FRANKEVER_IRRIGATION_CONTROLLER)
# Main device
entity_id = "sensor.test_name_average_temperature"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == "Test name"
# 3 zones with names, 3 with default names
zone_names = [
"Zone Name 1",
"Zone Name 2",
"Zone Name 3",
"Zone 4",
"Zone 5",
"Zone 6",
]
for zone_name in zone_names:
entity_id = f"valve.{zone_name.lower().replace(' ', '_')}"
state = hass.states.get(entity_id)
assert state
entry = entity_registry.async_get(entity_id)
assert entry
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.name == zone_name
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_block_channel_with_name(
hass: HomeAssistant,