mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add support for Shelly Gas Valve addon (#98705)
* Support Gas Valve * Treat opening and closing as open * Use set_state() * Change entity icon and name * Add valve state sensor * Closing == closed * Add translations for valve state entity * Valve state -> Valve status * Add tests; use control_result * Fix mypy error * Add missing "valve" to the Mock * Improve docstrings * Fix climate platform tests * Increase test coverage * Add mising return
This commit is contained in:
parent
c025244ac1
commit
17050a3286
@ -179,3 +179,5 @@ MAX_PUSH_UPDATE_FAILURES = 5
|
|||||||
PUSH_UPDATE_ISSUE_ID = "push_update_{unique}"
|
PUSH_UPDATE_ISSUE_ID = "push_update_{unique}"
|
||||||
|
|
||||||
NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"
|
NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}"
|
||||||
|
|
||||||
|
GAS_VALVE_OPEN_STATES = ("opening", "opened")
|
||||||
|
@ -312,6 +312,24 @@ SENSORS: Final = {
|
|||||||
value=lambda value: value,
|
value=lambda value: value,
|
||||||
extra_state_attributes=lambda block: {"self_test": block.selfTest},
|
extra_state_attributes=lambda block: {"self_test": block.selfTest},
|
||||||
),
|
),
|
||||||
|
("valve", "valve"): BlockSensorDescription(
|
||||||
|
key="valve|valve",
|
||||||
|
name="Valve status",
|
||||||
|
translation_key="valve_status",
|
||||||
|
icon="mdi:valve",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=[
|
||||||
|
"checking",
|
||||||
|
"closed",
|
||||||
|
"closing",
|
||||||
|
"failure",
|
||||||
|
"opened",
|
||||||
|
"opening",
|
||||||
|
"unknown",
|
||||||
|
],
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
removal_condition=lambda _, block: block.valve == "not_connected",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
REST_SENSORS: Final = {
|
REST_SENSORS: Final = {
|
||||||
|
@ -116,6 +116,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"valve_status": {
|
||||||
|
"state": {
|
||||||
|
"checking": "Checking",
|
||||||
|
"closed": "Closed",
|
||||||
|
"closing": "Closing",
|
||||||
|
"failure": "Failure",
|
||||||
|
"opened": "Opened",
|
||||||
|
"opening": "Opening",
|
||||||
|
"unknown": "[%key:component::shelly::entity::sensor::operation::state::unknown%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
"""Switch for Shelly."""
|
"""Switch for Shelly."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from aioshelly.block_device import Block
|
from aioshelly.block_device import Block
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import GAS_VALVE_OPEN_STATES
|
||||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
||||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
from .entity import (
|
||||||
|
BlockEntityDescription,
|
||||||
|
ShellyBlockAttributeEntity,
|
||||||
|
ShellyBlockEntity,
|
||||||
|
ShellyRpcEntity,
|
||||||
|
async_setup_block_attribute_entities,
|
||||||
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
async_remove_shelly_entity,
|
async_remove_shelly_entity,
|
||||||
get_device_entry_gen,
|
get_device_entry_gen,
|
||||||
@ -21,6 +29,19 @@ from .utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription):
|
||||||
|
"""Class to describe a BLOCK switch."""
|
||||||
|
|
||||||
|
|
||||||
|
GAS_VALVE_SWITCH = BlockSwitchDescription(
|
||||||
|
key="valve|valve",
|
||||||
|
name="Valve",
|
||||||
|
available=lambda block: block.valve not in ("failure", "checking"),
|
||||||
|
removal_condition=lambda _, block: block.valve in ("not_connected", "unknown"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -43,6 +64,17 @@ def async_setup_block_entry(
|
|||||||
coordinator = get_entry_data(hass)[config_entry.entry_id].block
|
coordinator = get_entry_data(hass)[config_entry.entry_id].block
|
||||||
assert coordinator
|
assert coordinator
|
||||||
|
|
||||||
|
# Add Shelly Gas Valve as a switch
|
||||||
|
if coordinator.model == "SHGS-1":
|
||||||
|
async_setup_block_attribute_entities(
|
||||||
|
hass,
|
||||||
|
async_add_entities,
|
||||||
|
coordinator,
|
||||||
|
{("valve", "valve"): GAS_VALVE_SWITCH},
|
||||||
|
BlockValveSwitch,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# In roller mode the relay blocks exist but do not contain required info
|
# In roller mode the relay blocks exist but do not contain required info
|
||||||
if (
|
if (
|
||||||
coordinator.model in ["SHSW-21", "SHSW-25"]
|
coordinator.model in ["SHSW-21", "SHSW-25"]
|
||||||
@ -94,6 +126,53 @@ def async_setup_rpc_entry(
|
|||||||
async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids)
|
async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockValveSwitch(ShellyBlockAttributeEntity, SwitchEntity):
|
||||||
|
"""Entity that controls a Gas Valve on Block based Shelly devices."""
|
||||||
|
|
||||||
|
entity_description: BlockSwitchDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: ShellyBlockCoordinator,
|
||||||
|
block: Block,
|
||||||
|
attribute: str,
|
||||||
|
description: BlockSwitchDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize valve."""
|
||||||
|
super().__init__(coordinator, block, attribute, description)
|
||||||
|
self.control_result: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""If valve is open."""
|
||||||
|
if self.control_result:
|
||||||
|
return self.control_result["state"] in GAS_VALVE_OPEN_STATES
|
||||||
|
|
||||||
|
return self.attribute_value in GAS_VALVE_OPEN_STATES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the icon."""
|
||||||
|
return "mdi:valve-open" if self.is_on else "mdi:valve-closed"
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Open valve."""
|
||||||
|
self.control_result = await self.set_state(go="open")
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Close valve."""
|
||||||
|
self.control_result = await self.set_state(go="close")
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_callback(self) -> None:
|
||||||
|
"""When device updates, clear control result that overrides state."""
|
||||||
|
self.control_result = None
|
||||||
|
|
||||||
|
super()._update_callback()
|
||||||
|
|
||||||
|
|
||||||
class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
|
class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity):
|
||||||
"""Entity that controls a relay on Block based Shelly devices."""
|
"""Entity that controls a relay on Block based Shelly devices."""
|
||||||
|
|
||||||
|
@ -131,6 +131,16 @@ MOCK_BLOCKS = [
|
|||||||
description="emeter_0",
|
description="emeter_0",
|
||||||
type="emeter",
|
type="emeter",
|
||||||
),
|
),
|
||||||
|
Mock(
|
||||||
|
sensor_ids={"valve": "closed"},
|
||||||
|
valve="closed",
|
||||||
|
channel="0",
|
||||||
|
description="valve_0",
|
||||||
|
type="valve",
|
||||||
|
set_state=AsyncMock(
|
||||||
|
side_effect=lambda go: {"state": "opening" if go == "open" else "closing"}
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
MOCK_CONFIG = {
|
MOCK_CONFIG = {
|
||||||
|
@ -32,6 +32,7 @@ from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data
|
|||||||
SENSOR_BLOCK_ID = 3
|
SENSOR_BLOCK_ID = 3
|
||||||
DEVICE_BLOCK_ID = 4
|
DEVICE_BLOCK_ID = 4
|
||||||
EMETER_BLOCK_ID = 5
|
EMETER_BLOCK_ID = 5
|
||||||
|
GAS_VALVE_BLOCK_ID = 6
|
||||||
ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name"
|
ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name"
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ async def test_climate_hvac_mode(
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
||||||
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
||||||
|
monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp")
|
||||||
await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01")
|
await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01")
|
||||||
|
|
||||||
# Make device online
|
# Make device online
|
||||||
@ -103,6 +105,7 @@ async def test_climate_set_temperature(
|
|||||||
"""Test climate set temperature service."""
|
"""Test climate set temperature service."""
|
||||||
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
||||||
|
monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp")
|
||||||
await init_integration(hass, 1, sleep_period=1000)
|
await init_integration(hass, 1, sleep_period=1000)
|
||||||
|
|
||||||
# Make device online
|
# Make device online
|
||||||
@ -144,6 +147,7 @@ async def test_climate_set_preset_mode(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test climate set preset mode service."""
|
"""Test climate set preset mode service."""
|
||||||
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
||||||
|
monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp")
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", None)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", None)
|
||||||
await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01")
|
await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01")
|
||||||
@ -198,6 +202,7 @@ async def test_block_restored_climate(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test block restored climate."""
|
"""Test block restored climate."""
|
||||||
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
||||||
|
monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp")
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
||||||
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
||||||
entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True)
|
entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True)
|
||||||
@ -261,6 +266,7 @@ async def test_block_restored_climate_us_customery(
|
|||||||
"""Test block restored climate with US CUSTOMATY unit system."""
|
"""Test block restored climate with US CUSTOMATY unit system."""
|
||||||
hass.config.units = US_CUSTOMARY_SYSTEM
|
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||||
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp")
|
||||||
|
monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp")
|
||||||
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0)
|
||||||
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp")
|
||||||
entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True)
|
entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True)
|
||||||
|
@ -9,6 +9,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
|||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_ICON,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
@ -16,10 +17,12 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import init_integration
|
from . import init_integration
|
||||||
|
|
||||||
RELAY_BLOCK_ID = 0
|
RELAY_BLOCK_ID = 0
|
||||||
|
GAS_VALVE_BLOCK_ID = 6
|
||||||
|
|
||||||
|
|
||||||
async def test_block_device_services(hass: HomeAssistant, mock_block_device) -> None:
|
async def test_block_device_services(hass: HomeAssistant, mock_block_device) -> None:
|
||||||
@ -226,3 +229,51 @@ async def test_rpc_auth_error(
|
|||||||
assert "context" in flow
|
assert "context" in flow
|
||||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||||
assert flow["context"].get("entry_id") == entry.entry_id
|
assert flow["context"].get("entry_id") == entry.entry_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_block_device_gas_valve(
|
||||||
|
hass: HomeAssistant, mock_block_device, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
"""Test block device Shelly Gas with Valve addon."""
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
await init_integration(hass, 1, "SHGS-1")
|
||||||
|
entity_id = "switch.test_name_valve"
|
||||||
|
|
||||||
|
entry = registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "123456789ABC-valve_0-valve"
|
||||||
|
|
||||||
|
assert hass.states.get(entity_id).state == STATE_OFF # valve is closed
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON # valve is open
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:valve-open"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_OFF # valve is closed
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:valve-closed"
|
||||||
|
|
||||||
|
monkeypatch.setattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "valve", "opened")
|
||||||
|
mock_block_device.mock_update()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON # valve is open
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:valve-open"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user