Skip to content

Commit

Permalink
Add backlight setting for entity (close #494)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Nov 20, 2024
1 parent 79c420b commit 3d43fd7
Show file tree
Hide file tree
Showing 18 changed files with 228 additions and 48 deletions.
4 changes: 2 additions & 2 deletions custom_components/yandex_smart_home/capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,12 @@ class StateCapability(Capability[CapabilityInstanceActionState], Protocol):
_hass: HomeAssistant
_entry_data: ConfigEntryData

def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, state: State):
def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, device_id: str, state: State):
"""Initialize a capability for the state."""
self._hass = hass
self._entry_data = entry_data

self.device_id = state.entity_id
self.device_id = device_id
self.state = state

@property
Expand Down
10 changes: 5 additions & 5 deletions custom_components/yandex_smart_home/capability_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ class ColorSettingCapability(StateCapability[ColorSettingCapabilityInstanceActio
type: CapabilityType = CapabilityType.COLOR_SETTING
instance: CapabilityInstance = ColorSettingCapabilityInstance.BASE

def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, state: State):
def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, device_id: str, state: State):
"""Initialize a capability for the state."""
super().__init__(hass, entry_data, state)
super().__init__(hass, entry_data, device_id, state)

self._color = RGBColorCapability(hass, entry_data, state)
self._temperature = ColorTemperatureCapability(hass, entry_data, state)
self._color = RGBColorCapability(hass, entry_data, device_id, state)
self._temperature = ColorTemperatureCapability(hass, entry_data, device_id, state)
self._scene = self._get_scene_capability()

@property
Expand Down Expand Up @@ -105,7 +105,7 @@ def _get_scene_capability(self) -> ColorSceneCapability:
)
return cast(ColorSceneCapability, custom_capability)

return ColorSceneStateCapability(self._hass, self._entry_data, self.state)
return ColorSceneStateCapability(self._hass, self._entry_data, self.device_id, self.state)

@property
def _capabilities(self) -> list[Capability[Any]]:
Expand Down
32 changes: 32 additions & 0 deletions custom_components/yandex_smart_home/capability_toggle.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_STOP_COVER,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_MUTE,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
)
Expand Down Expand Up @@ -51,6 +53,36 @@ class StateToggleCapability(ToggleCapability, StateCapability[ToggleCapabilityIn
pass


class BacklightCapability(StateToggleCapability):
"""Capability to represent state as backlight toggle."""

instance = ToggleCapabilityInstance.BACKLIGHT

@property
def supported(self) -> bool:
"""Test if the capability is supported."""
return True

def get_value(self) -> bool:
"""Return the current capability value."""
return self.state.state == STATE_ON

async def set_instance_state(self, context: Context, state: ToggleCapabilityInstanceActionState) -> None:
"""Change the capability state."""
if state.value:
service = SERVICE_TURN_ON
else:
service = SERVICE_TURN_OFF

await self._hass.services.async_call(
self.state.domain,
service,
{ATTR_ENTITY_ID: self.state.entity_id},
blocking=True,
context=context,
)


class MuteCapability(StateToggleCapability):
"""Capability to mute and unmute device."""

Expand Down
2 changes: 2 additions & 0 deletions custom_components/yandex_smart_home/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .color import ColorName, rgb_to_int
from .const import (
CONF_BACKLIGHT_ENTITY_ID,
CONF_BETA,
CONF_CLOUD_STREAM,
CONF_COLOR_PROFILE,
Expand Down Expand Up @@ -420,6 +421,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_ENTITY_PROPERTIES): [ENTITY_PROPERTY_SCHEMA],
vol.Optional(CONF_SUPPORT_SET_CHANNEL): cv.boolean,
vol.Optional(CONF_STATE_UNKNOWN): cv.boolean,
vol.Optional(CONF_BACKLIGHT_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_COLOR_PROFILE): cv.string,
vol.Optional(CONF_ERROR_CODE_TEMPLATE): cv.template,
vol.Optional(CONF_ENTITY_RANGE): ENTITY_RANGE_SCHEMA,
Expand Down
1 change: 1 addition & 0 deletions custom_components/yandex_smart_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
CONF_FEATURES = "features"
CONF_SUPPORT_SET_CHANNEL = "support_set_channel"
CONF_STATE_UNKNOWN = "state_unknown"
CONF_BACKLIGHT_ENTITY_ID = "backlight_entity_id"
CONF_ERROR_CODE_TEMPLATE = "error_code_template"
CONF_ENTITY_PROPERTY_TYPE = "type"
CONF_ENTITY_PROPERTY_ENTITY = "entity"
Expand Down
29 changes: 20 additions & 9 deletions custom_components/yandex_smart_home/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from homeassistant.helpers.template import Template

from custom_components.yandex_smart_home.const import (
CONF_BACKLIGHT_ENTITY_ID,
CONF_ENTITY_CUSTOM_MODES,
CONF_ENTITY_CUSTOM_RANGES,
CONF_ENTITY_CUSTOM_TOGGLES,
Expand All @@ -79,6 +80,7 @@
)
from .capability import STATE_CAPABILITIES_REGISTRY, Capability, DummyCapability, StateCapability
from .capability_custom import get_custom_capability
from .capability_toggle import BacklightCapability
from .helpers import ActionNotAllowed, APIError
from .property import STATE_PROPERTIES_REGISTRY, Property, StateProperty
from .property_custom import get_custom_property, get_event_platform_custom_property_type
Expand Down Expand Up @@ -192,6 +194,10 @@ def get_capabilities(self) -> list[Capability[Any]]:
capabilities: list[Capability[Any]] = []
disabled_capabilities: list[Capability[Any]] = []

def _append_capabilities(_capability: Capability[Any]) -> None:
if _capability.supported and _capability not in capabilities and _capability not in disabled_capabilities:
capabilities.append(_capability)

if (state_template := self._config.get(CONF_STATE_TEMPLATE)) is not None:
capabilities.append(
get_custom_capability(
Expand Down Expand Up @@ -227,17 +233,22 @@ def get_capabilities(self) -> list[Capability[Any]]:
self.id,
)

if custom_capability.supported and custom_capability not in capabilities:
capabilities.append(custom_capability)
_append_capabilities(custom_capability)

for CapabilityT in STATE_CAPABILITIES_REGISTRY:
state_capability = CapabilityT(self._hass, self._entry_data, self._state)
if (
state_capability.supported
and state_capability not in capabilities
and state_capability not in disabled_capabilities
):
capabilities.append(state_capability)
state_capability = CapabilityT(self._hass, self._entry_data, self.id, self._state)
_append_capabilities(state_capability)

if backlight_entity_id := self._config.get(CONF_BACKLIGHT_ENTITY_ID):
backlight_state = self._hass.states.get(backlight_entity_id)
if backlight_state and backlight_entity_id != self.id:
backlight_device = Device(self._hass, self._entry_data, backlight_state.entity_id, backlight_state)
for capability in backlight_device.get_capabilities():
if capability.type != CapabilityType.ON_OFF:
_append_capabilities(capability)

backlight_capability = BacklightCapability(self._hass, self._entry_data, self.id, backlight_state)
_append_capabilities(backlight_capability)

return capabilities

Expand Down
14 changes: 10 additions & 4 deletions custom_components/yandex_smart_home/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .cloud import CloudManager
from .color import ColorProfiles
from .const import (
CONF_BACKLIGHT_ENTITY_ID,
CONF_CLOUD_INSTANCE,
CONF_CLOUD_INSTANCE_CONNECTION_TOKEN,
CONF_CLOUD_INSTANCE_ID,
Expand All @@ -54,7 +55,7 @@
ConnectionType,
EntityId,
)
from .device import DeviceId
from .device import BacklightCapability, DeviceId, StateCapability
from .helpers import APIError, CacheStore, SmartHomePlatform
from .notifier import CloudNotifier, Notifier, NotifierConfig, YandexDirectNotifier
from .property import StateProperty
Expand Down Expand Up @@ -417,11 +418,13 @@ def _get_trackable_templates(self) -> dict[Template, list[CustomCapability | Cus

return templates

def _get_trackable_entity_states(self) -> dict[EntityId, list[tuple[DeviceId, type[StateProperty]]]]:
def _get_trackable_entity_states(
self,
) -> dict[EntityId, list[tuple[DeviceId, type[StateProperty | StateCapability[Any]]]]]:
"""Return entity capability and property class types to track state changes."""
states: dict[EntityId, list[tuple[DeviceId, type[StateProperty]]]] = {}
states: dict[EntityId, list[tuple[DeviceId, type[StateProperty | StateCapability[Any]]]]] = {}

def _states_append(_entity_id: str, _device_id: str, t: type[StateProperty]) -> None:
def _states_append(_entity_id: str, _device_id: str, t: type[StateProperty | StateCapability[Any]]) -> None:
states.setdefault(_entity_id, [])
states[_entity_id].append((_device_id, t))

Expand All @@ -434,4 +437,7 @@ def _states_append(_entity_id: str, _device_id: str, t: type[StateProperty]) ->
entity_id: str = property_config[CONF_ENTITY_PROPERTY_ENTITY]
_states_append(entity_id, device_id, event_platform_property)

if backlight_entity_id := entity_config.get(CONF_BACKLIGHT_ENTITY_ID):
_states_append(backlight_entity_id, device_id, BacklightCapability)

return states
18 changes: 18 additions & 0 deletions docs/config/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,24 @@
entity_id: switch.camera_aquarium
```

## Подсветка { id=backlight }

> Параметр: `backlight_entity_id`
Позволяет подключить к устройству осветительный прибор (например подсветка чайника). Все функции осветительного прибора (яркость, цвет, температура) будут так же добавлены к устройству.

В качестве подсветки могут выступать не только осветительные приборы (`light.*`), но и любые другие объекты, поддерживающие действия `turn_on` и `turn_off`.

!!! example "Пример"
```yaml
yandex_smart_home:
entity_config:
water_heater.kettle:
backlight_entity_id: light.kettle_backlight
climate.ac:
backlight_entity_id: switch.ac_backlight
```

## Ограничение уровня громкости { id=range }

> Параметр: `range`
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/valid-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ yandex_smart_home:
switch.r4s1_kettle_boil:
name: Чайник
room: Кухня
backlight_entity_id: light.r4s1_kettle_light
error_code_template: |
{% if capability.type == 'devices.capabilities.on_off' and capability.state.instance == 'on' and capability.state.value %}
{% if states('sensor.r4s1_kettle_water_level')|int(0) < 10 %}
Expand Down
2 changes: 1 addition & 1 deletion tests/test_capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def get_capabilities(
caps = []

for CapabilityT in STATE_CAPABILITIES_REGISTRY:
capability = CapabilityT(hass, entry_data, state)
capability = CapabilityT(hass, entry_data, state.entity_id, state)

if capability.type != capability_type or capability.instance != instance:
continue
Expand Down
20 changes: 10 additions & 10 deletions tests/test_capability_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ class MockModeCapabilityAShortIndexFallback(MockModeCapabilityA):

async def test_capability_mode_unsupported(hass: HomeAssistant, entry_data: MockConfigEntryData) -> None:
state = State("switch.test", STATE_OFF)
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is False

state = State("switch.test", STATE_OFF, {"modes_list": ["foo", "bar"]})
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True


async def test_capability_mode_auto_mapping(
hass: HomeAssistant, entry_data: MockConfigEntryData, caplog: pytest.LogCaptureFixture
) -> None:
state = State("switch.test", STATE_OFF, {"modes_list": ["mode_1", "mode_3", "mode_4", "eco", "mode_5"]})
cap = MockModeCapabilityAShortIndexFallback(hass, entry_data, state)
cap = MockModeCapabilityAShortIndexFallback(hass, entry_data, state.entity_id, state)

assert cap.supported is True
assert cap.supported_ha_modes == ["mode_1", "mode_3", "mode_4", "eco", "mode_5"]
Expand Down Expand Up @@ -148,15 +148,15 @@ async def test_capability_mode_custom_mapping(hass: HomeAssistant) -> None:
}
},
)
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True
assert cap.supported_ha_modes == ["mode_1", "mode_foo", "mode_bar", "americano"] # yeap, strange too
assert cap.supported_yandex_modes == [ModeCapabilityMode.ECO, ModeCapabilityMode.LATTE]


async def test_capability_mode_fallback_index(hass: HomeAssistant, entry_data: MockConfigEntryData) -> None:
state = State("switch.test", STATE_OFF, {"modes_list": ["some", "mode_1", "foo", "off"]})
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True
assert cap.supported_ha_modes == ["some", "mode_1", "foo", "off"]
assert cap.supported_yandex_modes == [
Expand All @@ -182,7 +182,7 @@ async def test_capability_mode_fallback_index(hass: HomeAssistant, entry_data: M
}
},
)
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True
assert cap.supported_yandex_modes == [
ModeCapabilityMode.FOWL,
Expand All @@ -191,7 +191,7 @@ async def test_capability_mode_fallback_index(hass: HomeAssistant, entry_data: M
]

state = State("switch.test", STATE_OFF, {"modes_list": [f"mode_{v}" for v in range(0, 11)]})
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True
assert cap.get_yandex_mode_by_ha_mode("mode_9") == "ten"
assert cap.get_yandex_mode_by_ha_mode("mode_11") is None
Expand All @@ -209,17 +209,17 @@ async def test_capability_mode_fallback_index(hass: HomeAssistant, entry_data: M
}
},
)
cap = MockModeCapabilityA(hass, entry_data, state)
cap = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap.supported is True
assert cap.supported_yandex_modes == ["americano", "baby_food"]


async def test_capability_mode_get_value(hass: HomeAssistant, entry_data: MockConfigEntryData) -> None:
state = State("switch.test", STATE_OFF, {"modes_list": ["mode_1", "mode_3"], "current_mode": "mode_1"})
cap_a = MockModeCapabilityA(hass, entry_data, state)
cap_a = MockModeCapabilityA(hass, entry_data, state.entity_id, state)
assert cap_a.get_value() == ModeCapabilityMode.FOWL

cap = MockModeCapability(hass, entry_data, state)
cap = MockModeCapability(hass, entry_data, state.entity_id, state)
assert cap.get_value() is None
cap.state.state = "mode_3"
assert cap.get_value() == ModeCapabilityMode.PUERH_TEA
Expand Down
8 changes: 4 additions & 4 deletions tests/test_capability_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ class MockCapabilityRandomAccess(MockCapability):
def support_random_access(self) -> bool:
return True

cap = MockCapability(hass, entry_data, State("switch.test", STATE_ON))
cap = MockCapability(hass, entry_data, "switch.test", State("switch.test", STATE_ON))
assert cap.retrievable is False
assert cap.parameters == RangeCapabilityParameters(instance=RangeCapabilityInstance.VOLUME, random_access=False)

cap = MockCapabilityRandomAccess(hass, entry_data, State("switch.test", STATE_ON))
cap = MockCapabilityRandomAccess(hass, entry_data, "switch.test", State("switch.test", STATE_ON))
assert cap.retrievable
assert cap.support_random_access
assert cap._range == RangeCapabilityRange(min=0.0, max=100.0, precision=1.0)
Expand Down Expand Up @@ -1112,8 +1112,8 @@ class MockCapabilityRelative(MockCapability):
def support_random_access(self) -> bool:
return False

cap_random = MockCapabilityRandom(hass, entry_data, State("switch.foo", STATE_OFF))
cap_relative = MockCapabilityRelative(hass, entry_data, State("switch.foo", STATE_OFF))
cap_random = MockCapabilityRandom(hass, entry_data, "switch.foo", State("switch.foo", STATE_OFF))
cap_relative = MockCapabilityRelative(hass, entry_data, "switch.foo", State("switch.foo", STATE_OFF))
cap_random.instance = cap_relative.instance = instance

assert cap_random.parameters.range is not None
Expand Down
Loading

0 comments on commit 3d43fd7

Please sign in to comment.