Skip to content

Commit 7640d77

Browse files
committed
feat: allow command selection based on features and other attributes
refactor: upgraded command selection engine
1 parent 3d09a3c commit 7640d77

File tree

3 files changed

+157
-52
lines changed

3 files changed

+157
-52
lines changed

custom_components/pandora_cas/button.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
PandoraCASEntity,
2323
PandoraCASEntityDescription,
2424
CommandOptions,
25-
parse_description_command_id,
25+
resolve_command_id,
26+
has_device_type,
2627
)
2728
from pandora_cas.device import PandoraOnlineDevice
2829
from pandora_cas.enums import PandoraDeviceTypes, CommandID
@@ -44,10 +45,10 @@ class PandoraCASButtonEntityDescription(
4445
PandoraCASButtonEntityDescription(
4546
key="erase_errors",
4647
name="Erase Errors",
47-
command={
48-
None: CommandID.ERASE_DTC,
49-
PandoraDeviceTypes.NAV12: CommandID.NAV12_RESET_ERRORS,
50-
},
48+
command=[
49+
(has_device_type(PandoraDeviceTypes.NAV12), CommandID.NAV12_RESET_ERRORS),
50+
(None, CommandID.ERASE_DTC),
51+
],
5152
entity_category=EntityCategory.DIAGNOSTIC,
5253
icon="mdi:eraser",
5354
),
@@ -159,9 +160,14 @@ async def async_press(self) -> None:
159160
raise HomeAssistantError(
160161
"Simultaneous commands not allowed, wait until command completes"
161162
)
162-
command_id = parse_description_command_id(
163-
self.entity_description.command, self.pandora_device.type
163+
command_id = resolve_command_id(
164+
self.pandora_device,
165+
self.entity_description.command,
166+
raise_exceptions=False,
164167
)
168+
if command_id is None:
169+
raise HomeAssistantError("could not determine command to run")
170+
165171
self._is_pressing = True
166172
await self.run_device_command(command_id)
167173

custom_components/pandora_cas/entity.py

+104-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Base entity platform for Pandora Car Alarm system component."""
2+
13
import asyncio
24
import dataclasses
35
import logging
@@ -23,6 +25,7 @@
2325
from homeassistant.config_entries import ConfigEntry
2426
from homeassistant.const import CONF_USERNAME, ATTR_DEVICE_ID
2527
from homeassistant.core import HomeAssistant, callback, Event
28+
from homeassistant.exceptions import HomeAssistantError
2629
from homeassistant.helpers.entity import EntityDescription, DeviceInfo, Entity
2730
from homeassistant.helpers.entity_platform import (
2831
AddEntitiesCallback,
@@ -38,6 +41,7 @@
3841
ATTR_COMMAND_ID,
3942
CONF_OFFLINE_AS_UNAVAILABLE,
4043
DEFAULT_WAITER_TIMEOUT,
44+
EVENT_TYPE_COMMAND,
4145
)
4246
from pandora_cas.device import PandoraOnlineDevice
4347
from pandora_cas.enums import PandoraDeviceTypes, CommandID, Features
@@ -47,24 +51,96 @@
4751

4852
_LOGGER: Final = logging.getLogger(__name__)
4953

54+
DeviceValidator = Callable[[PandoraOnlineDevice], bool]
55+
CommandType = CommandID | int | Callable[[PandoraOnlineDevice], Awaitable]
56+
CommandOptions = CommandType | Sequence[tuple[DeviceValidator | None, CommandType]]
57+
58+
59+
# noinspection PyShadowingNames
60+
def check_for_device(
61+
has_features: Features | None = None,
62+
not_features: Features | None = None,
63+
has_device_type: str | tuple[str, ...] | None = None,
64+
) -> DeviceValidator:
65+
"""
66+
Creates a checker that checks something is suitable for device.
67+
:param has_features: Has specified features.
68+
:param not_features:
69+
:param has_device_type:
70+
:return:
71+
"""
72+
if has_features is None and not_features is None and has_device_type is None:
73+
raise ValueError(
74+
"You must specify either has_features or not_features or has_device_type"
75+
)
76+
77+
if isinstance(has_device_type, str):
78+
has_device_type = (has_device_type,)
79+
80+
def _perform_check_on_device(device: PandoraOnlineDevice) -> bool:
81+
if has_device_type is not None:
82+
if device.type not in has_device_type:
83+
return False
84+
if not (has_features is None and not_features is None):
85+
if (features := device.features) is None:
86+
return False
87+
if not (has_features is None or has_features & features):
88+
return False
89+
if not (not_features is None or not (not_features & features)):
90+
return False
91+
return True
92+
93+
return _perform_check_on_device
94+
95+
96+
def has_features(features: Features) -> DeviceValidator:
97+
"""Shortcut for has_features argument."""
98+
return check_for_device(has_features=features)
99+
100+
101+
def not_features(features: Features) -> DeviceValidator:
102+
"""Shortcut for not_features argument."""
103+
return check_for_device(not_features=features)
104+
105+
106+
def has_device_type(device_type: str | tuple[str, ...]) -> DeviceValidator:
107+
"""Shortcut for has_device_type argument."""
108+
return check_for_device(has_device_type=device_type)
109+
50110

51-
def parse_description_command_id(value: Any, device_type: str | None = None) -> int:
111+
def resolve_command_id(
112+
device: PandoraOnlineDevice, value: Any, raise_exceptions: bool = False
113+
) -> int | None:
52114
"""Retrieve command from definition."""
53115
if value is None:
54-
raise NotImplementedError("command not defined")
116+
return None
55117

56-
if isinstance(value, Mapping):
57-
try:
58-
value = value[device_type]
59-
except KeyError:
60-
if device_type is None:
61-
raise NotImplementedError("command not defined")
118+
if isinstance(value, list):
119+
for condition, command in value:
120+
if condition is None:
121+
return command
62122
try:
63-
value = value[None]
64-
except KeyError:
65-
raise NotImplementedError("command not defined")
66-
67-
return value if callable(value) else int(value)
123+
if condition(device):
124+
return command
125+
except BaseException as exc:
126+
if raise_exceptions:
127+
raise
128+
_LOGGER.warning(
129+
f"Exception occurred while checking command for device {device}: {exc}",
130+
exc_info=exc,
131+
)
132+
return None
133+
134+
try:
135+
return value if callable(value) else int(value)
136+
except BaseException as exc:
137+
if raise_exceptions:
138+
raise
139+
_LOGGER.warning(
140+
f"Exception occurred while checking command for device {device}: {exc}",
141+
exc_info=exc,
142+
)
143+
return None
68144

69145

70146
async def async_platform_setup_entry(
@@ -139,10 +215,6 @@ def __post_init__(self):
139215
self.translation_key = self.key
140216

141217

142-
CommandType = CommandID | int | Callable[[PandoraOnlineDevice], Awaitable]
143-
CommandOptions = CommandType | Mapping[str, CommandType]
144-
145-
146218
class BasePandoraCASEntity(Entity):
147219
ENTITY_ID_FORMAT: ClassVar[str] = NotImplemented
148220

@@ -316,14 +388,21 @@ def _process_command_response(self, event: Union[Event, datetime]) -> None:
316388
def _add_command_listener(self, command: CommandOptions | None) -> None:
317389
if command is None:
318390
return None
319-
command_id = parse_description_command_id(command, self.pandora_device.type)
391+
392+
command_id = resolve_command_id(
393+
self.pandora_device,
394+
command,
395+
raise_exceptions=False,
396+
)
397+
320398
if not isinstance(command_id, int):
321399
return None
400+
322401
if (listeners := self._command_listeners) is None:
323402
self._command_listeners = listeners = []
324403
listeners.append(
325404
self.hass.bus.async_listen(
326-
event_type=f"{DOMAIN}_command",
405+
event_type=EVENT_TYPE_COMMAND,
327406
listener=self._process_command_response,
328407
event_filter=callback(
329408
lambda x: int(x.data[ATTR_COMMAND_ID]) == command_id
@@ -464,15 +543,19 @@ async def run_binary_command(self, enable: bool) -> None:
464543
:param enable: Whether to run 'on' or 'off'.
465544
"""
466545
# Determine command to run
467-
command_id = parse_description_command_id(
546+
command_id = resolve_command_id(
547+
self.pandora_device,
468548
(
469549
self.entity_description.command_on
470550
if enable or self.entity_description.command_off is None
471551
else self.entity_description.command_off
472552
),
473-
self.pandora_device.type,
553+
raise_exceptions=False,
474554
)
475555

556+
if command_id is None:
557+
raise HomeAssistantError("could not determine command to run")
558+
476559
self._is_turning_on = enable
477560
self._is_turning_off = not enable
478561

custom_components/pandora_cas/switch.py

+40-24
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
async_platform_setup_entry,
1919
PandoraCASBooleanEntityDescription,
2020
PandoraCASBooleanEntity,
21+
has_device_type,
2122
)
2223
from pandora_cas.data import CurrentState
2324
from pandora_cas.enums import PandoraDeviceTypes, CommandID, BitStatus, Features
@@ -66,14 +67,20 @@ class PandoraCASSwitchEntityDescription(
6667
icon_off="mdi:radiator-off",
6768
attribute=CurrentState.bit_state,
6869
flag=BitStatus.BLOCK_HEATER_ACTIVE,
69-
command_on={
70-
None: CommandID.TURN_ON_BLOCK_HEATER,
71-
PandoraDeviceTypes.NAV12: CommandID.NAV12_TURN_ON_BLOCK_HEATER,
72-
},
73-
command_off={
74-
None: CommandID.TURN_OFF_BLOCK_HEATER,
75-
PandoraDeviceTypes.NAV12: CommandID.NAV12_TURN_OFF_BLOCK_HEATER,
76-
},
70+
command_on=[
71+
(
72+
has_device_type(PandoraDeviceTypes.NAV12),
73+
CommandID.NAV12_TURN_ON_BLOCK_HEATER,
74+
),
75+
(None, CommandID.TURN_ON_BLOCK_HEATER),
76+
],
77+
command_off=[
78+
(
79+
has_device_type(PandoraDeviceTypes.NAV12),
80+
CommandID.NAV12_TURN_OFF_BLOCK_HEATER,
81+
),
82+
(None, CommandID.TURN_OFF_BLOCK_HEATER),
83+
],
7784
features=Features.HEATER,
7885
),
7986
PandoraCASSwitchEntityDescription(
@@ -95,14 +102,17 @@ class PandoraCASSwitchEntityDescription(
95102
icon_off="mdi:wrench",
96103
attribute=CurrentState.bit_state,
97104
flag=BitStatus.SERVICE_MODE_ACTIVE,
98-
command_on={
99-
None: CommandID.ENABLE_SERVICE_MODE,
100-
PandoraDeviceTypes.NAV12: CommandID.NAV12_ENABLE_SERVICE_MODE,
101-
},
102-
command_off={
103-
None: CommandID.DISABLE_SERVICE_MODE,
104-
PandoraDeviceTypes.NAV12: CommandID.DISABLE_SERVICE_MODE,
105-
},
105+
command_on=[
106+
(
107+
has_device_type(PandoraDeviceTypes.NAV12),
108+
CommandID.NAV12_ENABLE_SERVICE_MODE,
109+
),
110+
(None, CommandID.ENABLE_SERVICE_MODE),
111+
],
112+
command_off=[
113+
(has_device_type(PandoraDeviceTypes.NAV12), CommandID.DISABLE_SERVICE_MODE),
114+
(None, CommandID.DISABLE_SERVICE_MODE),
115+
],
106116
),
107117
PandoraCASSwitchEntityDescription(
108118
key="ext_channel",
@@ -120,14 +130,20 @@ class PandoraCASSwitchEntityDescription(
120130
icon="mdi:led-off",
121131
# icon_turning_on="",
122132
# icon_turning_off="",
123-
command_on={
124-
None: CommandID.ENABLE_STATUS_OUTPUT,
125-
PandoraDeviceTypes.NAV12: CommandID.NAV12_ENABLE_STATUS_OUTPUT,
126-
},
127-
command_off={
128-
None: CommandID.DISABLE_STATUS_OUTPUT,
129-
PandoraDeviceTypes.NAV12: CommandID.NAV12_DISABLE_STATUS_OUTPUT,
130-
},
133+
command_on=[
134+
(
135+
has_device_type(PandoraDeviceTypes.NAV12),
136+
CommandID.NAV12_ENABLE_STATUS_OUTPUT,
137+
),
138+
(None, CommandID.ENABLE_STATUS_OUTPUT),
139+
],
140+
command_off=[
141+
(
142+
has_device_type(PandoraDeviceTypes.NAV12),
143+
CommandID.NAV12_DISABLE_STATUS_OUTPUT,
144+
),
145+
(None, CommandID.DISABLE_STATUS_OUTPUT),
146+
],
131147
assumed_state=True,
132148
),
133149
PandoraCASSwitchEntityDescription(

0 commit comments

Comments
 (0)