Parse the wattage out of microwave power_level instead of a raw string
"700W"/"0W" was passed straight through as an untyped string sensor. Parse the numeral and set device_class/unit so HA treats it as a power measurement. Add a unit-handling step to the adding-device-support skill so future dumps get this right the first time.
This commit is contained in:
@@ -103,7 +103,35 @@ sub-polled between summary polls. Pick descriptor types from `entities.py`
|
||||
as a gap for a human, or ignore it with a documented reason — never invent an
|
||||
entity on a hunch (`ignored.py`'s rule).
|
||||
|
||||
## 5. Enum selects need translation support
|
||||
## 5. Parse units out of the value — don't ship them embedded in a string
|
||||
|
||||
Samsung reps sometimes encode a numeral and its unit as one string
|
||||
(`x.com.samsung.da.powerLevel: "700W"`; a `desired`/`current` temperature whose
|
||||
unit lives in a sibling field instead). The lazy fix — a plain `SensorDesc`
|
||||
with no `device_class`/`unit` that passes the raw string straight through —
|
||||
binds the href and looks done, but HA then sees text, not a measurement: no
|
||||
unit conversion, no long-term statistics, no graphing. This shipped once
|
||||
already (microwave `power_level` went out as the literal string `"700W"`
|
||||
before being caught in review) — treat that as the standard failure mode to
|
||||
check for, not a one-off.
|
||||
|
||||
Before wiring up a numeric-looking field:
|
||||
- **Check where the unit actually lives.** Embedded in the same string
|
||||
(`powerLevel`)? A fixed, undocumented assumption (most wattages/energy
|
||||
fields)? Or reported live in a sibling field (`/temperatures/vs/0`'s
|
||||
per-item `unit`, `'Celsius'`/`'Fahrenheit'`)? Don't guess C vs F or W vs kW
|
||||
without checking the dump.
|
||||
- **Parse the numeral in `value_fn`** (regex or strip the unit suffix) so the
|
||||
entity's state is a number, not `"700W"`.
|
||||
- **Set `unit`** (static) **or `unit_fn`** (reads a live sibling field — see
|
||||
`common.normalize_temp_unit`, used by both `fridge.py` and `oven.py` for a
|
||||
per-device C/F reading) **plus the matching `device_class`/`state_class`**
|
||||
so HA treats it as a real measurement.
|
||||
- A plain string `SensorDesc` (no unit/device_class) is still correct for
|
||||
genuinely non-numeric state (mode names, enum-like text) — reserve it for
|
||||
that, not as a shortcut past parsing a numeral.
|
||||
|
||||
## 6. Enum selects need translation support
|
||||
|
||||
Any select whose options are raw device codes (course/cycle, and code-valued
|
||||
settings) must render through translations, not Python:
|
||||
@@ -114,7 +142,7 @@ settings) must render through translations, not Python:
|
||||
(e.g. `"16": "Cotton"`). Codes with no entry render as the raw code — that's
|
||||
the cue to identify and name them.
|
||||
|
||||
## 6. Coverage discipline: bound or ignored
|
||||
## 7. Coverage discipline: bound or ignored
|
||||
|
||||
Every href in the dump must resolve, or the repair fires. If a resource isn't
|
||||
worth an entity, add it to `capabilities/ignored.py` (a no-entity `Capability`)
|
||||
@@ -128,7 +156,7 @@ friendlier href**.
|
||||
ignored because washers bind it. When only one family should ignore an href
|
||||
that another binds, scope the ignore to that family's registry.
|
||||
|
||||
## 7. Reuse before writing new code
|
||||
## 8. Reuse before writing new code
|
||||
|
||||
Check `common.py` (generic OCF: power, energy, alarms, water) and `laundry.py`
|
||||
(shared washer/dryer/dishwasher: buzzer, job status, `cycle_select` + course
|
||||
@@ -137,7 +165,7 @@ registry uses `fridge.FIRMWARE_UPDATE`; all three laundry families share
|
||||
`laundry.cycle_select`. If two families hand-roll the same helper, hoist it to a
|
||||
shared module rather than copying.
|
||||
|
||||
## 8. Lock it in
|
||||
## 9. Lock it in
|
||||
|
||||
1. Add a **scrubbed** fixture `tests/fixtures/<type>_device.json`
|
||||
(`{"device0": [ {devcol rep}, {href, rep}, ... ]}`) — replace serials, MACs,
|
||||
|
||||
@@ -11,8 +11,9 @@ re-declared here -- see by_type/microwave.py.
|
||||
and options-array toggles (only a Sound_On/Off slot on this dump -- no
|
||||
UpperLamp/fastpreheat/NaturalSteam like the oven family) are microwave-
|
||||
specific, so it gets its own capability here. /oven/vs/0 additionally
|
||||
reports a powerLevel field the oven family's cavity capability doesn't
|
||||
carry, so it also gets its own.
|
||||
reports a powerLevel field (a wattage with the unit embedded in the string,
|
||||
e.g. "700W") the oven family's cavity capability doesn't carry, so it also
|
||||
gets its own.
|
||||
|
||||
Cycle start/pause is deliberately not modeled: unlike the oven family
|
||||
(whose module docstring documents confirmed-unreliable local cycle-start
|
||||
@@ -27,6 +28,8 @@ below any plausible cook temperature, with no confirmed write contract.
|
||||
display string) is not modeled here -- every field is empty on this dump
|
||||
(device idle, no active cook program); see ignored.py.
|
||||
"""
|
||||
import re
|
||||
|
||||
from ..capability import Capability
|
||||
from ..entities import SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import normalize_temp_unit
|
||||
@@ -39,6 +42,18 @@ def _int(v):
|
||||
return None
|
||||
|
||||
|
||||
_POWER_LEVEL_RE = re.compile(r'^(-?\d+)W$')
|
||||
|
||||
|
||||
def _power_level_watts(v):
|
||||
"""'0'/'700W' -> 0/700 -- the unit is embedded in the string on every
|
||||
dump seen so far; strip it so the sensor reports a plain wattage."""
|
||||
if not isinstance(v, str):
|
||||
return None
|
||||
m = _POWER_LEVEL_RE.match(v)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def _cavity_temp_unit(rep):
|
||||
items = rep.get('x.com.samsung.da.items') or []
|
||||
unit = items[0].get('x.com.samsung.da.unit') if items else None
|
||||
@@ -111,10 +126,10 @@ MICROWAVE_CAVITY = Capability(
|
||||
entities=(
|
||||
SensorDesc(key='cavity_state', field='x.com.samsung.da.state',
|
||||
name='Cavity state'),
|
||||
# e.g. "0W"/"700W" -- unit is embedded in the string on every dump
|
||||
# seen so far; shown as reported rather than parsed apart.
|
||||
SensorDesc(key='power_level', field='x.com.samsung.da.powerLevel',
|
||||
name='Power level', icon='mdi:flash'),
|
||||
name='Power level', icon='mdi:flash', device_class='power',
|
||||
state_class='measurement', unit='W',
|
||||
value_fn=_power_level_watts),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -84,9 +84,16 @@ def test_cavity_state_reads_state_field():
|
||||
assert desc.value_fn('Ready') == 'Ready'
|
||||
|
||||
|
||||
def test_power_level_reads_raw_string():
|
||||
def test_power_level_parses_watts():
|
||||
desc = next(e for e in microwave.MICROWAVE_CAVITY.entities if e.key == 'power_level')
|
||||
assert desc.value_fn('700W') == '700W'
|
||||
assert desc.value_fn('700W') == 700
|
||||
assert desc.value_fn('0W') == 0
|
||||
|
||||
|
||||
def test_power_level_rejects_unparseable():
|
||||
desc = next(e for e in microwave.MICROWAVE_CAVITY.entities if e.key == 'power_level')
|
||||
assert desc.value_fn('') is None
|
||||
assert desc.value_fn(None) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user