Merge pull request #110 from mbillow/claude/device-support-issue-107-coffee

Add coffee-maker capabilities to the water purifier registry (issue #107)
This commit is contained in:
Marc Billow
2026-07-26 23:40:03 -05:00
committed by GitHub
8 changed files with 496 additions and 0 deletions
@@ -12,6 +12,8 @@ REGISTRY = DeviceRegistry(
water_purifier.DISPENSE,
water_purifier.STATUS,
water_purifier.FAVORITE_CAPACITY,
water_purifier.FAVORITE_HOTWATER,
water_purifier.COFFEE,
water_purifier.LOCK,
*water_purifier.COVERAGE,
]),
@@ -116,6 +116,50 @@ FAVORITE_CAPACITY = Capability(
),
)
# Coffee-capable variant (issue #107) -- same "favorite" enable-toggle +
# supported-list select shape as FAVORITE_CAPACITY above, but for the hot
# water dispensed alongside brewing rather than the pour capacity.
FAVORITE_HOTWATER = Capability(
href='/favorite/hotwater/vs/0',
poll_tier='cold',
entities=(
SwitchDesc(key='favorite_hotwater_enabled', field='x.com.samsung.da.switchHotwater',
icon='mdi:star-outline',
entity_category='config',
value_fn=lambda v: v != 'Locked',
write_fn=lambda p, rep, href=None: (
['favorite', 'hotwater', 'vs', '0'],
{'x.com.samsung.da.switchHotwater': 'Unlocked' if p == 'On' else 'Locked'})),
SelectDesc(key='favorite_hotwater_temperature',
field='x.com.samsung.da.favorite.defaultTemperature',
icon='mdi:thermometer',
entity_category='config',
options_field='x.com.samsung.da.favorite.supportedList',
write_fn=lambda p, rep, href=None: (
['favorite', 'hotwater', 'vs', '0'],
{'x.com.samsung.da.favorite.defaultTemperature': p})),
),
)
# Coffee-capable variant (issue #107). No 'x.com.samsung.da.' field prefix
# on this resource, unlike the rest of the water-purifier surface.
COFFEE = Capability(
href='/favorite/coffee/vs/0',
poll_tier='warm',
entities=(
SwitchDesc(key='favorite_coffee_enabled', field='favorite.activate',
icon='mdi:coffee-outline',
entity_category='config',
value_fn=lambda v: v == 'On',
write_fn=lambda p, rep, href=None: (
['favorite', 'coffee', 'vs', '0'],
{'favorite.activate': 'On' if p == 'On' else 'Off'})),
SensorDesc(key='coffee_brew_status', field='brew.status',
icon='mdi:coffee-outline',
entity_category='diagnostic'),
),
)
LOCK = Capability(
href='/status/lock/vs/0',
poll_tier='warm',
@@ -160,6 +204,14 @@ _WP_IGNORED = [
# Static support-flags blob (automation.supported.modes/options) -- no
# live "current automation setting" field to expose.
'/automation/waterpurifier/vs/0',
# Coffee-capable variant (issue #107). All four are static
# capability-advertisement blobs or empty -- no live "current recipe" /
# "current custom slot" field to expose, unlike /favorite/coffee/vs/0
# (COFFEE above), which does carry live brew status.
'/brand/recipe/info/vs/0', # revision + max-brand-count metadata
'/coffee/custom/recipe/vs/0', # publisher.support: allowed custom-recipe slot IDs
'/recipe/coffee/vs/0', # same publisher.support shape, no per-recipe content
'/recipe/coffee/deletion/vs/0', # empty {} on this dump
]
COVERAGE = [Capability(href=h) for h in _WP_IGNORED]
@@ -250,6 +250,9 @@
"favorite_capacity": {
"name": "Favorite capacity"
},
"favorite_hotwater_temperature": {
"name": "Favorite hot water temperature"
},
"finish_sound": {
"name": "Finish sound",
"state": {
@@ -487,6 +490,9 @@
"clean_level": {
"name": "Clean level"
},
"coffee_brew_status": {
"name": "Coffee brew status"
},
"connection_mode": {
"name": "Connection mode",
"state": {
@@ -759,6 +765,12 @@
"favorite_capacity_enabled": {
"name": "Favorite capacity"
},
"favorite_coffee_enabled": {
"name": "Favorite coffee"
},
"favorite_hotwater_enabled": {
"name": "Favorite hot water"
},
"fridge_sound": {
"name": "Sound"
},
@@ -250,6 +250,9 @@
"favorite_capacity": {
"name": "Favoriete capaciteit"
},
"favorite_hotwater_temperature": {
"name": "Favoriete temperatuur warm water"
},
"finish_sound": {
"name": "Eindgeluid",
"state": {
@@ -487,6 +490,9 @@
"clean_level": {
"name": "Reinigingsniveau"
},
"coffee_brew_status": {
"name": "Koffiezetstatus"
},
"connection_mode": {
"name": "Verbindingsmodus",
"state": {
@@ -759,6 +765,12 @@
"favorite_capacity_enabled": {
"name": "Favoriete capaciteit"
},
"favorite_coffee_enabled": {
"name": "Favoriete koffie"
},
"favorite_hotwater_enabled": {
"name": "Favoriet warm water"
},
"fridge_sound": {
"name": "Geluid"
},
+31
View File
@@ -0,0 +1,31 @@
{
"state_keys": [
"alarm_code",
"buzz_lock",
"coffee_brew_status",
"coldwater_lock",
"dispense_capacity",
"dispense_type",
"favorite_capacity",
"favorite_capacity_enabled",
"favorite_coffee_enabled",
"favorite_hotwater_enabled",
"favorite_hotwater_temperature",
"filter_clean_remain_time",
"filter_door_status",
"filter_status",
"filter_usage",
"firmware_update",
"hot_water_temperature",
"hotwater_lock",
"pouring",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"sterilize_last_time",
"sterilize_period",
"sterilize_plan_time",
"sterilize_run_time",
"waterpurifier_status"
]
}
+295
View File
@@ -0,0 +1,295 @@
{
"device0": [
{
"rt": [
"x.com.samsung.devcol",
"oic.wk.col"
],
"if": [
"oic.if.baseline",
"oic.if.ll",
"oic.if.b"
]
},
{
"href": "/alarms/vs/0",
"rep": {}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.result": "Success",
"x.com.samsung.da.error": [
"DA_ERROR_NONE"
],
"x.com.samsung.da.supportedActions": [
"Start"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2"
],
"x.com.samsung.da.modes": [
"WATERFILTER_DISABLE"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP2X_WATERPURIFIER_20K|00156641|900000000216130001088700000E0000",
"x.com.samsung.da.description": "TP2X_WATERPURIFIER_20K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "RP0",
"x.com.samsung.da.diagMinVersion": "1.0",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "WiFi Module",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02144A240904",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "Micom",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "24091109, 2409110D, 22092302, FFFFFFFF, 23030200, FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+00:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "",
"otnList": [
{
"type": "WIFI",
"modelId": "A-WPWW-TP2-22-COMMON",
"versions": [
"20240904"
],
"visVersion": "240904"
},
{
"type": "Micom",
"modelId": "03800015664100156041",
"versions": [
"24091109",
"2409110D"
],
"visVersion": "240911"
},
{
"type": "Micom",
"modelId": "038000156141FFFFFFFF",
"versions": [
"22092302",
"FFFFFFFF"
],
"visVersion": "220923"
},
{
"type": "Micom",
"modelId": "038000159141FFFFFFFF",
"versions": [
"23030200",
"FFFFFFFF"
],
"visVersion": "230302"
}
]
}
},
{
"href": "/filter/waterfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "90",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterStatus": "normal",
"x.com.samsung.da.lastResetDate": "2025-08-29T03:22:31",
"x.com.samsung.da.filterResetType": [
"replaceable"
]
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.coldwaterLock": "Unlocked",
"x.com.samsung.da.buzzLock": "Unlocked"
}
},
{
"href": "/setting/waterpurifier/vs/0",
"rep": {
"x.com.samsung.da.hotwaterLevel": "5",
"x.com.samsung.da.desiredType": "ambientwater",
"x.com.samsung.da.tempDesiredHotWater": "40",
"x.com.samsung.da.tempUnit": "C",
"x.com.samsung.da.desiredCapacity": "100",
"x.com.samsung.da.capacityUnit": "C",
"x.com.samsung.da.capacityResolution": "10",
"x.com.samsung.da.hotwaterRange": [
"40",
"90"
],
"x.com.samsung.da.desiredCapacityRange": [
"50",
"2000"
],
"x.com.samsung.da.supportedTypes": [
"ambientwater",
"coldwater",
"hotwater"
],
"x.com.samsung.da.pourStatus": "Off"
}
},
{
"href": "/status/waterpurifier/vs/0",
"rep": {
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.filterDoorStatus": "Close",
"x.com.samsung.da.sterilizeRunTime": "0",
"x.com.samsung.da.sterilizeStartTime": "2026-07-27T17:00:00",
"x.com.samsung.da.sterilizePeriod": "3",
"x.com.samsung.da.sterilizePlanTime": "2026-07-27T17:00:00",
"x.com.samsung.da.sterilizeLastTime": "2026-07-24T17:08:25",
"x.com.samsung.da.sterilizeMaxTime": "10",
"x.com.samsung.da.filterCleanRemainTime": "0",
"x.com.samsung.da.filterCleanMaxTime": "14"
}
},
{
"href": "/favorite/capacity/vs/0",
"rep": {
"x.com.samsung.da.switchCapacity": "On",
"x.com.samsung.da.defaultCapacity": "100",
"x.com.samsung.da.capacityList": [
"100",
"200",
"260",
"500",
"9999"
]
}
},
{
"href": "/favorite/hotwater/vs/0",
"rep": {
"x.com.samsung.da.switchHotwater": "Unlocked",
"x.com.samsung.da.favorite.defaultTemperature": "40",
"x.com.samsung.da.favorite.revision": "0",
"x.com.samsung.da.favorite.showList": [
"40",
"75",
"85",
"90"
],
"x.com.samsung.da.favorite.supportedList": [
"40",
"75",
"85",
"90"
]
}
},
{
"href": "/automation/waterpurifier/vs/0",
"rep": {
"automation.supported.modes": [
"0"
],
"automation.supported.options": [
"capacity"
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/recipe/coffee/vs/0",
"rep": {
"publisher.support": [
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/coffee/custom/recipe/vs/0",
"rep": {
"publisher.support": [
"1",
"2",
"3",
"4",
"5"
]
}
},
{
"href": "/favorite/coffee/vs/0",
"rep": {
"favorite.activate": "On",
"brew.status": "Suspend",
"favorite.enable.list": [
"0",
"1",
"2"
]
}
},
{
"href": "/recipe/coffee/deletion/vs/0",
"rep": {}
},
{
"href": "/brand/recipe/info/vs/0",
"rep": {
"revision": "0",
"brand.num.max": "1"
}
}
]
}
+15
View File
@@ -150,6 +150,21 @@ def test_registry_reproduces_golden_state_keys_for_water_purifier():
)
def test_registry_reproduces_golden_state_keys_for_water_purifier_coffee():
"""TP2X_WATERPURIFIER_20K coffee-capable variant (issue #107) adds
/favorite/coffee/vs/0, /favorite/hotwater/vs/0, and three static
coffee-recipe resources not present in issue #90's original dump."""
from tests.conftest import _load_device
resources = _load_device('water_purifier_coffee')
golden = json.loads((GOLDEN / 'water_purifier_coffee.json').read_text())
state_keys = _new_state_keys('water_purifier_coffee', resources)
assert set(state_keys) == set(golden['state_keys']), (
f"state_keys mismatch:\n"
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
)
def test_registry_reproduces_golden_state_keys_for_cooktop():
from tests.conftest import _load_device
resources = _load_device('cooktop')
+77
View File
@@ -138,3 +138,80 @@ def test_mode_hrefs_are_ignored_not_guessed():
ignored_hrefs = {cap.href for cap in water_purifier.COVERAGE}
assert '/mode/vs/0' in ignored_hrefs
assert '/automation/waterpurifier/vs/0' in ignored_hrefs
# ---------------------------------------------------------------------------
# Coffee-capable variant (issue #107) -- /favorite/coffee/vs/0 and
# /favorite/hotwater/vs/0, not present in issue #90's original dump.
# ---------------------------------------------------------------------------
def _water_purifier_coffee():
resources = _load_device('water_purifier_coffee')
info = resources['/information/vs/0']
reg = for_device_by_model(
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
)
return reg, resources
def _bound_coffee():
reg, resources = _water_purifier_coffee()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state_coffee():
bound, resources = _bound_coffee()
return flatten(bound, resources)
def _desc_coffee(key):
bound, _ = _bound_coffee()
return next(b.desc for b in bound if b.desc.key == key)
def test_coffee_variant_no_unbound_hrefs():
"""Every resource in the issue #107 dump binds or is covered, including
the four coffee-recipe hrefs not present in issue #90's dump."""
reg, resources = _water_purifier_coffee()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_coffee_variant_expected_state_keys_present():
state = _state_coffee()
for key in ('favorite_coffee_enabled', 'coffee_brew_status',
'favorite_hotwater_enabled', 'favorite_hotwater_temperature'):
assert key in state, key
def test_favorite_coffee_write_contract():
desc = _desc_coffee('favorite_coffee_enabled')
assert desc.write_fn('On', {}) == (
['favorite', 'coffee', 'vs', '0'], {'favorite.activate': 'On'})
assert desc.write_fn('Off', {}) == (
['favorite', 'coffee', 'vs', '0'], {'favorite.activate': 'Off'})
def test_favorite_hotwater_write_contract():
enabled = _desc_coffee('favorite_hotwater_enabled')
assert enabled.write_fn('On', {}) == (
['favorite', 'hotwater', 'vs', '0'], {'x.com.samsung.da.switchHotwater': 'Unlocked'})
assert enabled.write_fn('Off', {}) == (
['favorite', 'hotwater', 'vs', '0'], {'x.com.samsung.da.switchHotwater': 'Locked'})
def test_favorite_hotwater_temperature_options_come_from_live_supported_list():
desc = _desc_coffee('favorite_hotwater_temperature')
assert desc.options_field == 'x.com.samsung.da.favorite.supportedList'
def test_coffee_recipe_hrefs_are_ignored_not_guessed():
"""Static capability-advertisement blobs or empty resources -- no live
'current recipe'/'current custom slot' field to expose, per the 'don't
guess' rule."""
from custom_components.localthings.registry.capabilities import water_purifier
ignored_hrefs = {cap.href for cap in water_purifier.COVERAGE}
for href in ('/brand/recipe/info/vs/0', '/coffee/custom/recipe/vs/0',
'/recipe/coffee/vs/0', '/recipe/coffee/deletion/vs/0'):
assert href in ignored_hrefs, href