Add write_resource/read_resource services for probing write contracts (issue #300)
The options-flow "Debug write" panel could only ever do one write to one href per pass -- not enough for the issue #300 wall oven, whose board discards settings writes while idle and only keeps them once a cycle is already running. Finding what starts a cycle needs an ordered sequence of writes across resources, with real settle delays between them, and a way to check afterward whether anything actually held. - coordinator.py: async_raw_write_sequence owns a whole ordered sequence under one _session_lock hold (so a poll can't interleave mid-sequence), with per-step settle and an optional delayed verify_after re-read done outside the lock. async_raw_write is now a one-item wrapper over it, so tests/test_coordinator_raw_write.py keeps passing unmodified. Also adds async_raw_read, a live GET bypassing the cache -- staleness is exactly what makes revert-testing unreliable. - services.py (new): the two HA services. Device-target resolution scans loaded coordinators' MAIN/subdevice identifiers and requires exactly one match, so an area/label target can't silently fan a raw write out across several appliances. Canonical->actual href translation happens here, not in the coordinator, which stays subdevice-agnostic. - services.yaml (new): selectors/descriptions for both services, inline per HA's custom-integration support -- keeps translations/en.json's mirror test (test_translations.py) green without touching all 6 languages for a services block. New exception keys (write caps, device target resolution) still went into translations/*.json's existing exceptions section, mirrored across all 6 languages. - __init__.py: adds async_setup to register the services once, process-wide. - config_flow.py: the debug panel's async_step_debug_edit now calls write_resource instead of coord.async_raw_write directly, so there is exactly one code path that performs a raw write. - README.md: new Part 5 documenting both services, with a worked write_resource example; points the capability-gap section at them. tests/test_services.py (new): sequencing/ordering, settle timing, changed vs. held (the reverted case is issue #300's own symptom), exactly-one- device resolution, subdevice href translation, validation caps, and the options-flow panel end to end through the service.
This commit is contained in:
@@ -98,6 +98,70 @@ Each device has its own **Configure** option in Settings > Devices & Services, u
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Reading and writing resources directly
|
||||
|
||||
Two HA actions, `localthings.write_resource` and `localthings.read_resource`, talk to a device's OCF resources directly instead of through this integration's entity model. They exist for two overlapping jobs: pinning down a device-specific write contract (the reverse-engineering work `docs/investigations/` and the provenance comments throughout `registry/capabilities/` are all about), and driving a resource this integration doesn't model as an entity yet, without waiting on a release.
|
||||
|
||||
Both target a device (`target: device:`, filtered to `integration: localthings` in the picker) and resolve to exactly one appliance — targeting an area or label that expands to more than one LocalThings device is rejected rather than silently fanned out across all of them. `href` is always canonical (e.g. `/mode/vs/0`); if the device you targeted is a subdevice — an oven's second cavity, an AC's second indoor unit — it's translated to the real on-the-wire href for you (`/mode/vs/1`, say), and the response reports both forms so there's no ambiguity about what was actually sent.
|
||||
|
||||
`write_resource` exists because a single write, one at a time, isn't enough to probe some boards. Issue #300's Samsung wall oven answers `2.04 Changed` to a settings write while idle and then silently reverts it — the write only sticks once a cycle is already running. Finding what actually triggers a cycle needs an *ordered sequence* of writes to different resources, with real delays between them, and a way to check afterward whether anything actually held:
|
||||
|
||||
```yaml
|
||||
action: localthings.write_resource
|
||||
target:
|
||||
device_id: abc123...
|
||||
data:
|
||||
writes:
|
||||
- href: /course/vs/0
|
||||
payload:
|
||||
x.com.samsung.da.course: "01"
|
||||
settle: 3
|
||||
- href: /mode/vs/0
|
||||
payload:
|
||||
x.com.samsung.da.mode: Bake
|
||||
settle: 5
|
||||
- href: /power/vs/0
|
||||
payload:
|
||||
x.com.samsung.da.power: "On"
|
||||
verify_after: 30
|
||||
```
|
||||
|
||||
Each write in `writes` (1-10 of them) needs `href` and a non-empty `payload`, sent verbatim as a partial-rep POST — this bypasses the remote-control-off block and every `write_fn`/`validate_fn` a normal entity write goes through, and sends exactly the fields you give it, so it can misconfigure your appliance if you get it wrong. `settle` (0-30s, default 0) is how long to wait *after* that write before starting the next one. The whole sequence runs under a single lock, so a routine poll can't land in the middle of it and blur which write is responsible for what the device does next.
|
||||
|
||||
The response has one `results` entry per write, with `before`/`after` reps and a `changed` flag (every key/value in `payload` present and equal in the immediate readback):
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "abc123...",
|
||||
"results": [
|
||||
{"href": "/course/vs/0", "actual_href": "/course/vs/0", "code": "2.04", "raw_code": 68,
|
||||
"accepted": true, "before": {...}, "after": {...}, "changed": true},
|
||||
...
|
||||
],
|
||||
"verified": {
|
||||
"/power/vs/0": {"code": "2.05", "raw_code": 69, "rep": {...}, "held": false}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`verify_after` (0-60s, default 0, omit to skip) is what actually answers the "did it stick" question: after the sequence finishes, it waits that long and then re-reads every distinct href the sequence touched, reporting the result under `verified`, keyed by canonical href. `changed` tells you the write was accepted and reflected immediately; `held` tells you whether it was still there N seconds later, or whether the board quietly put it back — issue #300's exact symptom. Where an href was written more than once in a sequence, `held` compares against the *last* payload sent to it.
|
||||
|
||||
`read_resource` is the read half, and it's deliberately not just a cache lookup:
|
||||
|
||||
```yaml
|
||||
action: localthings.read_resource
|
||||
target:
|
||||
device_id: abc123...
|
||||
data:
|
||||
href: /mode/vs/0
|
||||
```
|
||||
|
||||
returning `{"href", "actual_href", "code", "raw_code", "rep"}` off a **live GET straight from the device**, not the cache — which can be up to a poll interval stale, exactly the staleness that would make `held` above meaningless. Omit `href` and you get `{"resources": {href: rep, ...}}`, the cached snapshot of everything this integration currently tracks on that device, with no GET at all — useful for seeing what's there before you start writing to it, without hammering the appliance.
|
||||
|
||||
The **Debug write** panel under a device's Configure menu (Part 4) is the friendlier single-write path over this same machinery — pick an href, type a payload, see the result — for when you don't need a sequence.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Docker Compose dev environment
|
||||
@@ -135,6 +199,8 @@ custom_components/localthings/
|
||||
coordinator.py Polling + push update coordination, stale-state fallback, write dispatch
|
||||
observe.py CoAP OBSERVE (push-mode) support layered on the coordinator
|
||||
diagnostics.py Redacted diagnostics download (device state + coverage metadata)
|
||||
services.py write_resource/read_resource actions (device resolution, href translation)
|
||||
services.yaml Selectors/descriptions for the two services above
|
||||
const.py Domain, config keys, probe ports
|
||||
entity.py Base entity wiring capability registry -> HA entity
|
||||
sensor.py / binary_sensor.py / switch.py / number.py / select.py / button.py / time.py / fan.py / climate.py / water_heater.py
|
||||
@@ -172,6 +238,11 @@ email, access tokens, device IDs, MAC addresses, serial numbers) before it's gen
|
||||
directly to a new issue using the linked device-support template. This is the fastest way to help add or expand
|
||||
support for hardware the maintainers don't have.
|
||||
|
||||
When a diagnostics dump alone isn't enough to pin down how a resource actually behaves — whether a write sticks,
|
||||
what order things need to happen in, whether the device reverts a change on its own — the `localthings.write_resource`
|
||||
and `localthings.read_resource` actions from Part 5 are the tool for probing it directly and reporting back what
|
||||
you found.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new appliance type
|
||||
|
||||
Reference in New Issue
Block a user