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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user