feat: add shared common capabilities
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from ..capability import Capability
|
||||
from . import common
|
||||
|
||||
|
||||
def _is_capability(v):
|
||||
return isinstance(v, Capability)
|
||||
|
||||
|
||||
ALL = [
|
||||
*[v for v in vars(common).values() if _is_capability(v)],
|
||||
]
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Capabilities shared across multiple appliance families.
|
||||
|
||||
rt strings verified against live device dumps:
|
||||
/kidslock/vs/0 -> x.com.samsung.da.operation
|
||||
/remotectrl/vs/0 -> x.com.samsung.da.configuration
|
||||
/power/vs/0 -> x.com.samsung.da.operation (shares rt with kidslock)
|
||||
/alarms/vs/0 -> x.com.samsung.da.alarms
|
||||
/energy/consumption/vs/0 -> x.com.samsung.da.energyconsumption
|
||||
/water/consumption/vs/0 -> x.com.samsung.da.waterconsumption
|
||||
/filter/waterfilter/vs/0 -> x.com.samsung.da.filter.water
|
||||
|
||||
NOTE: KIDS_LOCK and POWER share the same rt (x.com.samsung.da.operation).
|
||||
When building a registry dict keyed by rt, only one can be present.
|
||||
They target different hrefs (/kidslock/vs/0 vs /power/vs/0) with different
|
||||
field keys — at runtime the absent field returns None and is filtered.
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SensorDesc
|
||||
|
||||
|
||||
def _num(v):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _clamp_power(v):
|
||||
n = _num(v)
|
||||
return 0.0 if (n is not None and n < 0) else n
|
||||
|
||||
|
||||
def _wh_to_kwh(v):
|
||||
n = _num(v)
|
||||
return round(n / 1000.0, 2) if n is not None else None
|
||||
|
||||
|
||||
def _ml_to_l(v):
|
||||
n = _num(v)
|
||||
return round(n / 1000.0, 1) if n is not None else None
|
||||
|
||||
|
||||
def _last_alarm_code(items):
|
||||
if items and isinstance(items, list):
|
||||
return items[-1].get('x.com.samsung.da.code')
|
||||
return None
|
||||
|
||||
|
||||
KIDS_LOCK = Capability(
|
||||
rt='x.com.samsung.da.operation',
|
||||
entities=(
|
||||
BinarySensorDesc(key='child_lock', field='x.com.samsung.da.kidsLock',
|
||||
name='Child lock', device_class='lock',
|
||||
value_fn=lambda v: v != 'Ready'),
|
||||
),
|
||||
)
|
||||
|
||||
REMOTE_CONTROL = Capability(
|
||||
rt='x.com.samsung.da.configuration',
|
||||
entities=(
|
||||
BinarySensorDesc(key='remote_control',
|
||||
field='x.com.samsung.da.remoteControlEnabled',
|
||||
name='Remote control', device_class='connectivity',
|
||||
value_fn=lambda v: str(v).lower() == 'true'),
|
||||
),
|
||||
)
|
||||
|
||||
# POWER shares rt with KIDS_LOCK (x.com.samsung.da.operation).
|
||||
# Do not include both in the same registry dict simultaneously.
|
||||
POWER = Capability(
|
||||
rt='x.com.samsung.da.operation',
|
||||
entities=(
|
||||
BinarySensorDesc(key='power_state', field='x.com.samsung.da.power',
|
||||
name='Power', device_class='power',
|
||||
value_fn=lambda v: v == 'On'),
|
||||
),
|
||||
)
|
||||
|
||||
ALARMS = Capability(
|
||||
rt='x.com.samsung.da.alarms',
|
||||
entities=(
|
||||
SensorDesc(key='alarm_code', field='x.com.samsung.da.items',
|
||||
name='Alarm code', icon='mdi:alert',
|
||||
entity_category='diagnostic', value_fn=_last_alarm_code),
|
||||
),
|
||||
)
|
||||
|
||||
ENERGY_METER = Capability(
|
||||
rt='x.com.samsung.da.energyconsumption',
|
||||
entities=(
|
||||
SensorDesc(key='power_watts', field='x.com.samsung.da.instantaneousPower',
|
||||
name='Power', device_class='power', state_class='measurement',
|
||||
unit='W', value_fn=_clamp_power),
|
||||
SensorDesc(key='energy_kwh', field='x.com.samsung.da.cumulativePower',
|
||||
name='Energy', device_class='energy',
|
||||
state_class='total_increasing', unit='kWh', value_fn=_wh_to_kwh),
|
||||
),
|
||||
)
|
||||
|
||||
WATER_METER = Capability(
|
||||
rt='x.com.samsung.da.waterconsumption',
|
||||
entities=(
|
||||
SensorDesc(key='water_liters', field='x.com.samsung.da.cumulativeWater',
|
||||
name='Water consumption', device_class='water',
|
||||
state_class='total_increasing', unit='L', icon='mdi:water',
|
||||
value_fn=_ml_to_l),
|
||||
),
|
||||
)
|
||||
|
||||
WATER_FILTER = Capability(
|
||||
rt='x.com.samsung.da.filter.water',
|
||||
entities=(
|
||||
SensorDesc(key='filter_usage', field='x.com.samsung.da.filterUsage',
|
||||
name='Filter usage', unit='%', state_class='measurement',
|
||||
icon='mdi:filter'),
|
||||
SensorDesc(key='filter_status', field='x.com.samsung.da.filterStatus',
|
||||
name='Filter status', icon='mdi:filter-check'),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
from samsung_appliance.registry.capabilities import common
|
||||
from samsung_appliance.registry.discovery import discover
|
||||
|
||||
|
||||
def _reg():
|
||||
return {c.rt: c for c in (
|
||||
common.KIDS_LOCK, common.REMOTE_CONTROL, common.ALARMS,
|
||||
common.ENERGY_METER, common.WATER_METER, common.WATER_FILTER,
|
||||
)}
|
||||
|
||||
|
||||
def test_kids_lock_binary_value_fn():
|
||||
desc = common.KIDS_LOCK.entities[0]
|
||||
assert desc.value_fn('Lock') is True
|
||||
assert desc.value_fn('Ready') is False
|
||||
|
||||
|
||||
def test_energy_clamps_negative_power(fridge_resources):
|
||||
# instantaneousPower can read negative at idle; must clamp to 0.
|
||||
pw = next(e for e in common.ENERGY_METER.entities if e.key == 'power_watts')
|
||||
assert pw.value_fn(-500.0) == 0.0
|
||||
assert pw.value_fn(93.0) == 93.0
|
||||
|
||||
|
||||
def test_common_caps_discover_on_dishwasher(dishwasher_resources):
|
||||
bound = discover(dishwasher_resources, _reg())
|
||||
keys = {b.desc.key for b in bound}
|
||||
assert 'child_lock' in keys
|
||||
assert 'remote_control' in keys
|
||||
assert 'power_watts' in keys
|
||||
Reference in New Issue
Block a user