Merge pull request #334 from mbillow/claude/issue-triage-d7hz7u

Issue triage: filterUsage percentage fix, TP1X_REF_21K auto-door + winecellar, dual-cavity range routing (#330, #328, #324)
This commit is contained in:
Marc Billow
2026-08-08 23:09:29 -04:00
committed by GitHub
29 changed files with 2519 additions and 38 deletions
+1 -1
View File
@@ -12,5 +12,5 @@
"pyOpenSSL>=23.0",
"smartthings-local>=0.1.2"
],
"version": "0.20.0"
"version": "0.21.0"
}
@@ -225,13 +225,16 @@ _OIC_TYPE_TO_KEY: dict[str, str] = {
"oic.d.dishwasher": "dishwasher",
"oic.d.dryer": "dryer",
"oic.d.oven": "oven",
"oic.d.range": "range", # issue #324 -- oven+cooktop combo, no /information/vs/0
"oic.d.refrigerator": "refrigerator",
"oic.d.krefrigerator": "refrigerator", # issue #328 -- kimchi refrigerator
"oic.d.washer": "washer",
"x.com.st.d.airqualitysensor": "air_monitor",
"x.com.st.d.dehumidifier": "dehumidifier",
"x.com.st.d.hood": "range_hood", # AHD-WW-TP1-22-COMMON
"x.com.st.d.stickcleaner": "vacuum_station",
"x.com.st.d.steamcloset": "air_dresser",
"x.com.st.d.winecellar": "refrigerator", # issue #328 -- same TP1X_REF_21K board
}
@@ -14,6 +14,10 @@ REGISTRY = DeviceRegistry(
fridge.DOOR_ALERT,
common.WATER_FILTER,
fridge.AIR_FILTER,
fridge.DEODOR_FILTER,
fridge.AUTO_DOOR_TIMER,
fridge.WINECELLAR_PANTRY_ZONE,
fridge.WINECELLAR_INFO,
dishwasher.DIAGNOSIS,
fridge.ICEMAKER_NIGHTTIME,
fridge.FLEX_ZONE,
@@ -44,5 +48,6 @@ REGISTRY = DeviceRegistry(
fridge.DOOR_GENERIC,
fridge.KIMCHI_ZONE,
fridge.KIMCHI_DOOR_GENERIC,
fridge.AUTO_DOOR_VARIANT,
],
)
@@ -22,7 +22,7 @@ from ..entities import (
SwitchDesc,
)
from . import common
from .common import filter_usage_percent, normalize_temp_unit
from .common import filter_usage_hours, filter_usage_percent, normalize_temp_unit
from .laundry import option_write
@@ -854,16 +854,17 @@ AIR_FILTER = Capability(
icon="mdi:air-filter",
entity_category="diagnostic",
),
# Lifetime hour counter, resets only on filter replacement.
# Lifetime hour counter, resets only on filter replacement. Derived
# from the percentage and filterCapacity (issue #330) -- filterUsage
# itself is not an hour count.
SensorDesc(
key="air_filter_usage_hours",
field="x.com.samsung.da.filterUsage",
rep_fn=filter_usage_hours,
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
),
# Locally writable alarm threshold (see _threshold_write); only
# surfaces where supportedFilterDesiredUsage is advertised.
@@ -925,13 +926,12 @@ AIR_FILTER_PM1 = Capability(
),
SensorDesc(
key="air_filter_pm1_usage_hours",
field="x.com.samsung.da.filterUsage",
rep_fn=filter_usage_hours,
device_class="duration",
state_class="total_increasing",
unit_fn=_filter_unit,
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=_int,
exists_fn=_has_filter_field("x.com.samsung.da.filterUsage"),
),
SelectDesc(
@@ -72,15 +72,25 @@ def epoch_to_utc(value):
def filter_usage_percent(rep):
"""Filter usage as a percentage of rated capacity. Several families
report `filterUsage` as a raw count in `filterCapacityUnit` (e.g. 100 of
a 500-hour capacity), so a plain value with a '%' unit would be wrong.
Returns None when capacity is missing/zero."""
used = _num(rep.get("x.com.samsung.da.filterUsage"))
"""Filter usage as a percentage. `filterUsage` is already 0-100 on every
family confirmed so far, including ARTIK051_PRAC (issue #330): its own
fixture and three live heads all show `filterStatus == 'wash'` at
`filterUsage == '100'` regardless of `filterCapacity` (60/224/500 across
other families' fixtures), which only holds if `filterUsage` is already
a percent -- dividing by capacity again would read that filter as fresh
at 20%."""
return int_or_none(rep.get("x.com.samsung.da.filterUsage"))
def filter_usage_hours(rep):
"""Elapsed filter hours, derived from the percentage and capacity rather
than read off `filterUsage` directly -- `filterUsage` is a percent, not
an hour count (issue #330). Returns None when capacity is missing/zero."""
pct = filter_usage_percent(rep)
cap = _num(rep.get("x.com.samsung.da.filterCapacity"))
if used is None or not cap:
if pct is None or not cap:
return None
return round(used / cap * 100)
return round(pct / 100 * cap, 1)
def normalize_temp_unit(raw, default="°F"):
@@ -326,9 +326,154 @@ STATUS_LOCK = Capability(
value_fn=lambda v: v == "On",
write_fn=_status_lock_write("x.com.samsung.da.device.sound"),
),
# Auto Door Open's own voice/sound feedback toggles (issue #328,
# TP1X_REF_21K family) -- siblings of auto_door_opener above, not
# duplicates of fridge_sound (device.sound is the general appliance
# beep, these two gate ado's own prompts). Only seen on the
# auto-door-equipped variants (single/kimchi/winecellar), not the
# earlier TP1X_REF_21K dumps that predate that feature -- gated on
# each field's own presence rather than assumed universal.
SwitchDesc(
key="auto_door_voice_control",
field="x.com.samsung.da.ado.voicecontrol",
icon="mdi:microphone",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=_status_lock_write("x.com.samsung.da.ado.voicecontrol"),
exists_fn=lambda rep, resources: "x.com.samsung.da.ado.voicecontrol" in rep,
),
SwitchDesc(
key="auto_door_sound_control",
field="x.com.samsung.da.ado.soundcontrol",
icon="mdi:volume-medium",
entity_category="config",
value_fn=lambda v: v == "On",
write_fn=_status_lock_write("x.com.samsung.da.ado.soundcontrol"),
exists_fn=lambda rep, resources: "x.com.samsung.da.ado.soundcontrol" in rep,
),
),
)
# Auto Door Open's paired delay setting (issue #328, TP1X_REF_21K family):
# how long the door stays held open before it re-closes, on/off itself is
# STATUS_LOCK.auto_door_opener above. Same discrete-options-select shape as
# DEFINITE_TEMPERATURE_COOLER/FREEZER. Unit unconfirmed -- no supportedList
# field states it and the report carried no app screenshot -- so this is a
# raw-code select rather than a guessed seconds/minutes NumberDesc.
def _auto_door_timer_write(p, rep, href=None):
return ["autodoor", "timer", "vs", "0"], {"x.com.samsung.da.time.desired": p}
AUTO_DOOR_TIMER = Capability(
href="/autodoor/timer/vs/0",
poll_tier="warm",
entities=(
SelectDesc(
key="auto_door_timer",
field="x.com.samsung.da.time.desired",
icon="mdi:timer-outline",
translation_key="auto_door_timer",
entity_category="config",
options_field="x.com.samsung.da.time.supportedOptions",
write_fn=_auto_door_timer_write,
),
),
)
# /autodoor/<variant>/vs/0 -- one per fridge sub-type sharing the Auto Door
# Open feature (single-door, kimchi, winecellar seen so far; issue #328).
# Each reports only x.com.samsung.da.ado.openOptions, declaring which open
# styles that variant supports -- every dump seen so far carries exactly
# one option ('Single') with no paired desired/current field to make a
# choice against, the same "no real choice to expose yet" shape as
# ignored.py's /mode/0. Bound with no entities to record coverage; revisit
# if a device ever reports more than one option.
#
# A pattern cap rather than one entry per variant: match_fn (not just the
# prefix) is what actually gates this, so a future variant href needs no
# code change to stay covered, and /autodoor/timer/vs/0's own exact-href
# AUTO_DOOR_TIMER above always wins for that href regardless (discover()
# only falls through to pattern caps when no exact cap matched). This is
# registry-scoped, not global -- unlike ignored.IGNORED, the unknown-
# device-type fallback never reaches it, so the prefix caveat in
# ignored.py's own docstring doesn't apply here.
AUTO_DOOR_VARIANT = Capability(
href=None,
href_prefix="/autodoor/",
match_fn=lambda rep, resources: "x.com.samsung.da.ado.openOptions" in rep,
)
# Wine-cellar variant (x.com.st.d.winecellar, issue #328) of the same
# deodorizing filter AIR_FILTER models -- filterUsage/filterStatus at a
# different href, same 0-100-percentage-already shape (see AIR_FILTER's own
# comment). filterUsage reads '-1' on the only dump seen (filterStatus
# 'normal'), relayed as-is rather than special-cased -- no second dump to
# confirm whether that's a real sentinel or this unit just not tracking it.
# Own 'deodor_'-prefixed keys rather than reusing AIR_FILTER.entities
# verbatim -- same collision AIR_FILTER's own 'air_' prefix was chosen to
# avoid against WATER_FILTER, and both filters are plausible on one unit
# (this device's own board reports an internal air filter on other
# TP1X_REF_21K variants).
DEODOR_FILTER = Capability(
href="/filter/deodorfilter/vs/0",
poll_tier="cold",
entities=(
SensorDesc(
key="deodor_filter_usage",
field="x.com.samsung.da.filterUsage",
unit="%",
state_class="measurement",
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=int_or_none,
),
SensorDesc(
key="deodor_filter_status",
field="x.com.samsung.da.filterStatus",
device_class="enum",
options=("normal", "wash", "replace"),
translation_key="filter_status",
icon="mdi:air-filter",
entity_category="diagnostic",
value_fn=lambda v: v.lower() if isinstance(v, str) else v,
),
),
)
# Wine-cellar multi-compartment pantry select (issue #328): same
# mode/supportedOptions shape as PANTRY_ZONE, at its own href with a 5-way
# option set (Processed_Meat/Cheese/Nuts/Fruit/Wine) instead of PANTRY_ZONE's.
# Only a "one" instance seen -- not generalized to a pattern cap, same
# discipline as PANTRY_ZONE itself.
def _winecellar_pantry_write(p, rep, href=None):
return ["status", "winecellar", "pantry", "one", "vs", "0"], {"x.com.samsung.da.mode": p}
WINECELLAR_PANTRY_ZONE = Capability(
href="/status/winecellar/pantry/one/vs/0",
poll_tier="warm",
entities=(
SelectDesc(
key="winecellar_pantry_zone_mode",
field="x.com.samsung.da.mode",
icon="mdi:glass-wine",
translation_key="winecellar_pantry_zone_mode",
entity_category="config",
options_field="x.com.samsung.da.supportedOptions",
write_fn=_winecellar_pantry_write,
),
),
)
# Wine-cellar internal table-revision marker (issue #328) -- versioning
# metadata, not appliance state. Same treatment as ignored.py's
# /wm/setinfo/vs/0.
WINECELLAR_INFO = Capability(href="/information/winecellar/vs/0")
# /defrost/delay/vs/0 is the writable toggle to postpone a scheduled
# defrost. /defrost/block/vs/0 is unrelated: despite the "block" naming,
# live dumps confirm DEFROST_BLOCK_ON means the defrost cycle is actively
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "Práh upozornění na filtr"
},
"auto_door_timer": {
"name": "Časovač automatického otevírání dvířek"
},
"beverage_zone_mode": {
"name": "Režim zóny nápojů",
"state": {
@@ -472,9 +475,11 @@
"kimchi_storage_crunfch": "Křupavé kimči",
"kimchi_storage_buy": "Kupované kimči",
"storage_fridge_normal": "Chlazení",
"storage_fridge": "Chlazení",
"storage_fridge_cold": "Chlazení, silné",
"storage_fridge_warm": "Chlazení, slabé",
"storage_freezer_normal": "Mražení",
"storage_freezer": "Mražení",
"storage_freezer_cold": "Mražení, silné",
"storage_freezer_warm": "Mražení, slabé",
"kimchi_ripe_low_temp": "Zrání kimči, nízká teplota",
@@ -487,7 +492,8 @@
"storage_fresh_cereal": "Obiloviny",
"storage_fridge_drink": "Nápoje",
"storage_fresh_wine": "Víno",
"storage_fresh_potato_banana": "Brambory a banány"
"storage_fresh_potato_banana": "Brambory a banány",
"newmode_kimchi_0000": "Nový režim"
}
},
"pantry_zone_mode": {
@@ -498,6 +504,16 @@
"fdr_drinks": "Nápoje"
}
},
"winecellar_pantry_zone_mode": {
"name": "Režim spíže",
"state": {
"processed_meat": "Zpracované maso",
"cheese": "Sýr",
"nuts": "Ořechy",
"fruit": "Ovoce",
"wine": "Víno"
}
},
"range_burner_power_level": {
"name": "Výkon hořáku {number}",
"state": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "Hodiny využití filtru"
},
"deodor_filter_usage": {
"name": "Využití filtru"
},
"air_quality_standard": {
"name": "Norma kvality vzduchu"
},
@@ -1147,6 +1166,12 @@
"auto_door_opener": {
"name": "Automatické otevírání dvířek"
},
"auto_door_sound_control": {
"name": "Zvuk automatického otevírání dvířek"
},
"auto_door_voice_control": {
"name": "Hlasové ovládání automatického otevírání dvířek"
},
"auto_release_dry": {
"name": "Automatické uvolnění při sušení"
},
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "Filteralarm-Schwellenwert"
},
"auto_door_timer": {
"name": "Timer für automatischen Türöffner"
},
"beverage_zone_mode": {
"name": "Getränkezonen-Modus",
"state": {
@@ -467,6 +470,16 @@
"fdr_drinks": "Getränke"
}
},
"winecellar_pantry_zone_mode": {
"name": "Vorratszonen-Modus",
"state": {
"processed_meat": "Wurst/Aufschnitt",
"cheese": "Käse",
"nuts": "Nüsse",
"fruit": "Obst",
"wine": "Wein"
}
},
"range_burner_power_level": {
"name": "Brenner {number} Leistungsstufe",
"state": {
@@ -678,9 +691,11 @@
"kimchi_storage_crunfch": "Knackiges Kimchi",
"kimchi_storage_buy": "Gekauftes Kimchi",
"storage_fridge_normal": "Kühlen",
"storage_fridge": "Kühlen",
"storage_fridge_cold": "Kühlen, stark",
"storage_fridge_warm": "Kühlen, schwach",
"storage_freezer_normal": "Gefrieren",
"storage_freezer": "Gefrieren",
"storage_freezer_cold": "Gefrieren, stark",
"storage_freezer_warm": "Gefrieren, schwach",
"kimchi_ripe_low_temp": "Kimchi-Reifung, niedrige Temperatur",
@@ -693,7 +708,8 @@
"storage_fresh_cereal": "Getreide",
"storage_fridge_drink": "Getränke",
"storage_fresh_wine": "Wein",
"storage_fresh_potato_banana": "Kartoffeln und Bananen"
"storage_fresh_potato_banana": "Kartoffeln und Bananen",
"newmode_kimchi_0000": "Neuer Modus"
}
},
"energy_saving_mode": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "Filternutzungsstunden"
},
"deodor_filter_usage": {
"name": "Filternutzung"
},
"air_quality_standard": {
"name": "Luftqualitätsstandard"
},
@@ -1144,6 +1163,12 @@
"auto_door_opener": {
"name": "Automatischer Türöffner"
},
"auto_door_sound_control": {
"name": "Ton des automatischen Türöffners"
},
"auto_door_voice_control": {
"name": "Sprachsteuerung des automatischen Türöffners"
},
"auto_release_dry": {
"name": "Automatische Trocknungsfreigabe"
},
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "Filter alarm threshold"
},
"auto_door_timer": {
"name": "Auto door open timer"
},
"beverage_zone_mode": {
"name": "Beverage zone mode",
"state": {
@@ -474,9 +477,11 @@
"storage_fridge_normal": "Fridge",
"storage_fridge_cold": "Fridge, strong",
"storage_fridge_warm": "Fridge, weak",
"storage_fridge": "Fridge",
"storage_freezer_normal": "Freezer",
"storage_freezer_cold": "Freezer, strong",
"storage_freezer_warm": "Freezer, weak",
"storage_freezer": "Freezer",
"kimchi_ripe_low_temp": "Kimchi ripening, low temperature",
"kimchi_ripe_normal_temp": "Kimchi ripening, room temperature",
"kimchi_ripe_kkakdugi": "Kkakdugi ripening",
@@ -487,7 +492,8 @@
"storage_fresh_cereal": "Grains",
"storage_fridge_drink": "Beverages",
"storage_fresh_wine": "Wine",
"storage_fresh_potato_banana": "Potato & banana"
"storage_fresh_potato_banana": "Potato & banana",
"newmode_kimchi_0000": "New mode"
}
},
"pantry_zone_mode": {
@@ -498,6 +504,16 @@
"fdr_drinks": "Drinks"
}
},
"winecellar_pantry_zone_mode": {
"name": "Pantry zone mode",
"state": {
"processed_meat": "Processed meat",
"cheese": "Cheese",
"nuts": "Nuts",
"fruit": "Fruit",
"wine": "Wine"
}
},
"range_burner_power_level": {
"name": "Burner {number} power level",
"state": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "Filter usage hours"
},
"deodor_filter_usage": {
"name": "Filter usage"
},
"air_quality_standard": {
"name": "Air quality standard"
},
@@ -1147,6 +1166,12 @@
"auto_door_opener": {
"name": "Auto door opener"
},
"auto_door_sound_control": {
"name": "Auto door sound"
},
"auto_door_voice_control": {
"name": "Auto door voice control"
},
"auto_release_dry": {
"name": "Auto release dry"
},
@@ -374,6 +374,9 @@
"air_filter_threshold": {
"name": "Umbral de alarma del filtro"
},
"auto_door_timer": {
"name": "Temporizador de apertura automática de puerta"
},
"beverage_zone_mode": {
"name": "Modo de zona de bebidas",
"state": {
@@ -621,9 +624,11 @@
"kimchi_storage_crunfch": "Kimchi crujiente",
"kimchi_storage_buy": "Kimchi comprado",
"storage_fridge_normal": "Frigorífico",
"storage_fridge": "Frigorífico",
"storage_fridge_cold": "Frigorífico, fuerte",
"storage_fridge_warm": "Frigorífico, suave",
"storage_freezer_normal": "Congelador",
"storage_freezer": "Congelador",
"storage_freezer_cold": "Congelador, fuerte",
"storage_freezer_warm": "Congelador, suave",
"kimchi_ripe_low_temp": "Maduración kimchi, baja temperatura",
@@ -636,7 +641,8 @@
"storage_fresh_cereal": "Cereales",
"storage_fridge_drink": "Bebidas",
"storage_fresh_wine": "Vino",
"storage_fresh_potato_banana": "Patata y plátano"
"storage_fresh_potato_banana": "Patata y plátano",
"newmode_kimchi_0000": "Modo nuevo"
}
},
"pantry_zone_mode": {
@@ -647,6 +653,16 @@
"fdr_drinks": "Bebidas"
}
},
"winecellar_pantry_zone_mode": {
"name": "Modo de zona de despensa",
"state": {
"processed_meat": "Embutidos",
"cheese": "Queso",
"nuts": "Frutos secos",
"fruit": "Fruta",
"wine": "Vino"
}
},
"range_burner_power_level": {
"name": "Nivel de potencia del fuego {number}",
"state": {
@@ -891,6 +907,9 @@
"air_filter_usage_hours": {
"name": "Horas de uso del filtro"
},
"deodor_filter_usage": {
"name": "Uso del filtro"
},
"air_quality_standard": {
"name": "Estándar de calidad del aire"
},
@@ -1296,6 +1315,12 @@
"auto_door_opener": {
"name": "Apertura automática de puerta"
},
"auto_door_sound_control": {
"name": "Sonido de apertura automática de puerta"
},
"auto_door_voice_control": {
"name": "Control por voz de apertura automática de puerta"
},
"auto_release_dry": {
"name": "Liberación automática de secado"
},
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "Soglia allarme filtro"
},
"auto_door_timer": {
"name": "Timer apertura automatica sportello"
},
"beverage_zone_mode": {
"name": "Modalità zona bevande",
"state": {
@@ -472,9 +475,11 @@
"kimchi_storage_crunfch": "Kimchi croccante",
"kimchi_storage_buy": "Kimchi acquistato",
"storage_fridge_normal": "Frigorifero",
"storage_fridge": "Frigorifero",
"storage_fridge_cold": "Frigorifero, forte",
"storage_fridge_warm": "Frigorifero, debole",
"storage_freezer_normal": "Congelatore",
"storage_freezer": "Congelatore",
"storage_freezer_cold": "Congelatore, forte",
"storage_freezer_warm": "Congelatore, debole",
"kimchi_ripe_low_temp": "Maturazione kimchi, bassa temperatura",
@@ -487,7 +492,8 @@
"storage_fresh_cereal": "Cereali",
"storage_fridge_drink": "Bevande",
"storage_fresh_wine": "Vino",
"storage_fresh_potato_banana": "Patate e banane"
"storage_fresh_potato_banana": "Patate e banane",
"newmode_kimchi_0000": "Nuova modalità"
}
},
"pantry_zone_mode": {
@@ -498,6 +504,16 @@
"fdr_drinks": "Bevande"
}
},
"winecellar_pantry_zone_mode": {
"name": "Modalità zona dispensa",
"state": {
"processed_meat": "Salumi",
"cheese": "Formaggio",
"nuts": "Frutta secca",
"fruit": "Frutta",
"wine": "Vino"
}
},
"range_burner_power_level": {
"name": "Livello potenza fornello {number}",
"state": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "Ore di utilizzo filtro"
},
"deodor_filter_usage": {
"name": "Utilizzo filtro"
},
"air_quality_standard": {
"name": "Standard qualità aria"
},
@@ -1147,6 +1166,12 @@
"auto_door_opener": {
"name": "Apertura automatica sportello"
},
"auto_door_sound_control": {
"name": "Suono apertura automatica sportello"
},
"auto_door_voice_control": {
"name": "Controllo vocale apertura automatica sportello"
},
"auto_release_dry": {
"name": "Apertura automatica per asciugatura"
},
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "필터 알림 기준"
},
"auto_door_timer": {
"name": "오토 오픈 도어 타이머"
},
"beverage_zone_mode": {
"name": "음료 보관 모드",
"state": {
@@ -472,9 +475,11 @@
"kimchi_storage_crunfch": "아삭 김치",
"kimchi_storage_buy": "구입 김치",
"storage_fridge_normal": "냉장",
"storage_fridge": "냉장",
"storage_fridge_cold": "냉장 강",
"storage_fridge_warm": "냉장 약",
"storage_freezer_normal": "냉동",
"storage_freezer": "냉동",
"storage_freezer_cold": "냉동 강",
"storage_freezer_warm": "냉동 약",
"kimchi_ripe_low_temp": "김치 저온 숙성",
@@ -487,7 +492,8 @@
"storage_fresh_cereal": "곡류",
"storage_fridge_drink": "음료",
"storage_fresh_wine": "와인",
"storage_fresh_potato_banana": "감자 및 바나나"
"storage_fresh_potato_banana": "감자 및 바나나",
"newmode_kimchi_0000": "새 모드"
}
},
"pantry_zone_mode": {
@@ -498,6 +504,16 @@
"fdr_drinks": "음료"
}
},
"winecellar_pantry_zone_mode": {
"name": "팬트리실 모드",
"state": {
"processed_meat": "가공육",
"cheese": "치즈",
"nuts": "견과류",
"fruit": "과일",
"wine": "와인"
}
},
"range_burner_power_level": {
"name": "{number}번 화구 화력",
"state": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "필터 사용 시간"
},
"deodor_filter_usage": {
"name": "필터 사용량"
},
"air_quality_standard": {
"name": "공기질 기준"
},
@@ -1147,6 +1166,12 @@
"auto_door_opener": {
"name": "오토 오픈 도어"
},
"auto_door_sound_control": {
"name": "오토 오픈 도어 소리"
},
"auto_door_voice_control": {
"name": "오토 오픈 도어 음성 안내"
},
"auto_release_dry": {
"name": "자동 문 열림 건조"
},
@@ -225,6 +225,9 @@
"air_filter_threshold": {
"name": "Filteralarmdrempel"
},
"auto_door_timer": {
"name": "Timer automatische deuropener"
},
"beverage_zone_mode": {
"name": "Modus drankenzone",
"state": {
@@ -472,9 +475,11 @@
"kimchi_storage_crunfch": "Knapperige kimchi",
"kimchi_storage_buy": "Gekochte kimchi",
"storage_fridge_normal": "Koelkast",
"storage_fridge": "Koelkast",
"storage_fridge_cold": "Koelkast, sterk",
"storage_fridge_warm": "Koelkast, zwak",
"storage_freezer_normal": "Vriezer",
"storage_freezer": "Vriezer",
"storage_freezer_cold": "Vriezer, sterk",
"storage_freezer_warm": "Vriezer, zwak",
"kimchi_ripe_low_temp": "Kimchi rijpen, lage temperatuur",
@@ -487,7 +492,8 @@
"storage_fresh_cereal": "Granen",
"storage_fridge_drink": "Dranken",
"storage_fresh_wine": "Wijn",
"storage_fresh_potato_banana": "Aardappelen en bananen"
"storage_fresh_potato_banana": "Aardappelen en bananen",
"newmode_kimchi_0000": "Nieuwe modus"
}
},
"pantry_zone_mode": {
@@ -498,6 +504,16 @@
"fdr_drinks": "Dranken"
}
},
"winecellar_pantry_zone_mode": {
"name": "Modus voorraadzone",
"state": {
"processed_meat": "Vleeswaren",
"cheese": "Kaas",
"nuts": "Noten",
"fruit": "Fruit",
"wine": "Wijn"
}
},
"range_burner_power_level": {
"name": "Vermogensniveau brander {number}",
"state": {
@@ -745,6 +761,9 @@
"air_filter_usage_hours": {
"name": "Filterverbruik (uren)"
},
"deodor_filter_usage": {
"name": "Filterverbruik"
},
"air_quality_standard": {
"name": "Luchtkwaliteitsnorm"
},
@@ -1147,6 +1166,12 @@
"auto_door_opener": {
"name": "Automatische deuropener"
},
"auto_door_sound_control": {
"name": "Geluid automatische deuropener"
},
"auto_door_voice_control": {
"name": "Spraakbediening automatische deuropener"
},
"auto_release_dry": {
"name": "Auto Release Dry"
},
+8 -1
View File
@@ -70,7 +70,7 @@ class FakeCoapSession:
pass
def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]):
def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list], device_types=()):
"""Run the *whole* subdevice-aware discovery pipeline against fixture
data, HA-free -- mirrors exactly what LocalThingsCoordinator does across
_enumerate_subdevices_blocking + _run_discovery (issue #177), so a test
@@ -78,6 +78,12 @@ def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]):
it. See the adding-device-support skill's section 2 for the plain
(non-subdevice) equivalent this extends.
`device_types` is the master's own /oic/d `rt` (see
discover_partitioned's `oic_device_types` param) -- only needed for a
board with no /information/vs/0 at all to route from (issue #324's
range, whose modelNum-based fallback has nothing to read), so it
defaults to () for every fixture that resolves by board token instead.
Returns `(bound, materialized, skipped, full_resources, device_type_name)`:
- `bound`: every BoundEntity, main + every materialized subdevice.
- `materialized`/`skipped`: Subdevice / SkippedSubdevice lists straight from
@@ -101,6 +107,7 @@ def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]):
candidates,
resolve,
CAPABILITIES,
oic_device_types=device_types,
)
return bound, materialized, skipped, full_resources, device_type_name
+39
View File
@@ -0,0 +1,39 @@
{
"state_keys": [
"alarm_code",
"child_lock",
"cloud_connected",
"cook_time",
"cooktop_on_alert",
"cooktop_running_state",
"current_temp_c",
"cycle_active",
"door_open",
"energy_saving",
"finish_time",
"firmware_update",
"lamp",
"machine_state",
"operation_time_minutes",
"oven_mode",
"oven_setpoint",
"oven_state",
"power_switch",
"progress_percentage",
"remote_control",
"sound",
"subdevice1_cloud_connected",
"subdevice1_cook_time",
"subdevice1_current_temp_c",
"subdevice1_cycle_active",
"subdevice1_finish_time",
"subdevice1_machine_state",
"subdevice1_operation_time_minutes",
"subdevice1_oven_mode",
"subdevice1_oven_setpoint",
"subdevice1_oven_state",
"subdevice1_progress_percentage",
"subdevice1_sound",
"warming_center_state"
]
}
@@ -0,0 +1,24 @@
{
"state_keys": [
"ai_energy_level",
"alarm_code",
"auto_door_opener",
"auto_door_timer",
"auto_door_voice_control",
"defrost_active",
"door_onedoorfreezer_open",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"freezer_setpoint",
"freezer_temperature",
"fridge_sound",
"power_energy_kwh",
"power_watts",
"rapid_freezing",
"rapid_fridge",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
@@ -0,0 +1,22 @@
{
"state_keys": [
"alarm_code",
"auto_door_opener",
"auto_door_timer",
"auto_door_voice_control",
"door_onedoorkimchi_open",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"fridge_sound",
"onedoor_mode",
"onedoor_rack_count",
"onedoor_ripening_remaining",
"onedoor_ripening_status",
"power_energy_kwh",
"power_watts",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status"
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"state_keys": [
"alarm_code",
"auto_door_opener",
"auto_door_sound_control",
"auto_door_timer",
"auto_door_voice_control",
"cabinet_light_dim",
"cabinet_light_switch",
"deodor_filter_status",
"deodor_filter_usage",
"door_winecellar_open",
"energy_kwh",
"energy_saved_kwh",
"firmware_update",
"fridge_sound",
"power_energy_kwh",
"power_watts",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
"winecellar_bottom_setpoint",
"winecellar_pantry_zone_mode",
"winecellar_top_setpoint"
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,343 @@
{
"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": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Freezer",
"x.com.samsung.da.desired": "-19",
"x.com.samsung.da.current": "-19",
"x.com.samsung.da.maximum": "-17",
"x.com.samsung.da.minimum": "-23",
"x.com.samsung.da.unit": "Celsius"
}
]
}
},
{
"href": "/temperature/current/freezer/0",
"rep": {
"range": [
-23.0,
-17.0
],
"units": "C",
"temperature": -19.0
}
},
{
"href": "/temperature/desired/freezer/0",
"rep": {
"range": [
-23.0,
-17.0
],
"units": "C",
"temperature": -19.0
}
},
{
"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": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "41042",
"x.com.samsung.da.cumulativePower": "466708",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPower": "4",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativeSavedPower": "15759"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_REF_OUTDOOR_CONTROL_V2"
]
}
},
{
"href": "/mode/0",
"rep": {
"supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_REF_OUTDOOR_CONTROL_V2"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00168541|00080023001713104100000041010000",
"x.com.samsung.da.description": "TP1X_REF_21K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"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": "RO6",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"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": "250422",
"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": "24101709, 23031302, 24062000, FFFFFFFF",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+09:00",
"x.com.samsung.supprtedtype": 1
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
]
}
},
{
"href": "/door/onedoorfreezer/vs/0",
"rep": {
"x.com.samsung.da.openState": "Close"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.countryCode": "",
"x.com.samsung.da.region": ""
}
},
{
"href": "/refrigeration/0",
"rep": {
"defrost": false,
"rapidFreeze": false,
"rapidCool": false
}
},
{
"href": "/refrigeration/vs/0",
"rep": {
"x.com.samsung.da.rapidFreezing": "Off"
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 2,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "2",
"x.com.samsung.da.override": "Not_Supported",
"x.com.samsung.da.durationminutes": "1441",
"x.com.samsung.da.start": "2026-08-06T00:34:47Z",
"x.com.samsung.da.realSaving": "On"
}
},
{
"href": "/bespoke/vs/0",
"rep": {
"x.com.samsung.da.BespokeProduct": "On"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "",
"otnList": [
{
"type": "WIFI",
"modelId": "A-RFWW-TP1-23-COMMON",
"versions": [
"20250422"
],
"visVersion": "250422"
},
{
"type": "Micom",
"modelId": "02800016854100168441",
"versions": [
"24101709",
"23031302"
],
"visVersion": "241017"
},
{
"type": "Micom",
"modelId": "028070655341FFFFFFFF",
"versions": [
"24062000",
"FFFFFFFF"
],
"visVersion": "240620"
}
]
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.ado.voicecontrol": "Off",
"x.com.samsung.da.device.sound": "On"
}
},
{
"href": "/energy/ailevel/vs/0",
"rep": {
"aiLevel": "1",
"supportedAiLevel": [
"1",
"2"
]
}
},
{
"href": "/autodoor/timer/vs/0",
"rep": {
"x.com.samsung.da.time.desired": "1",
"x.com.samsung.da.time.supportedOptions": [
"1",
"2",
"3",
"4",
"5",
"6"
]
}
},
{
"href": "/autodoor/single/vs/0",
"rep": {
"x.com.samsung.da.ado.openOptions": [
"Single"
]
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"ble_ocf",
null
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
@@ -0,0 +1,332 @@
{
"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": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "53539",
"x.com.samsung.da.cumulativePower": "174590",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPower": "4",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativeSavedPower": "0"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_KIMCHI_OUTDOOR_CONTROL"
],
"x.com.samsung.da.modes": [
"KIMCHI_KIMCHI_STORAGE_NORMAL",
"KIMCHI_RIPE_REMAIN_[0]:[0]",
"KIMCHIT_BOX_COUNT_[0]",
"KIMCHIM_BOX_COUNT_[0]",
"KIMCHIB_BOX_COUNT_[0]",
"KIMCHI_BOX_COUNT_[8]"
],
"x.com.samsung.da.supportedOptions": [
"KIMCHI_KIMCHI_STORAGE_NORMAL_[0]:[0]",
"KIMCHI_KIMCHI_STORAGE_COLD_[0]:[0]",
"KIMCHI_KIMCHI_STORAGE_WARM_[0]:[0]",
"KIMCHI_KIMCHI_STORAGE_CRUNFCH_[0]:[0]",
"KIMCHI_KIMCHI_STORAGE_BUY_[0]:[0]",
"KIMCHI_STORAGE_FRIDGE_[0]:[0]",
"KIMCHI_STORAGE_FREEZER_[0]:[0]",
"KIMCHI_KIMCHI_RIPE_NORMAL_TEMP_[2]:[12]",
"KIMCHI_KIMCHI_RIPE_LOW_TEMP_[5]:[17]"
]
}
},
{
"href": "/mode/0",
"rep": {
"supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"18K_KIMCHI_OUTDOOR_CONTROL"
],
"modes": [
"KIMCHI_KIMCHI_STORAGE_NORMAL",
"KIMCHI_RIPE_REMAIN_[0]:[0]",
"KIMCHIT_BOX_COUNT_[0]",
"KIMCHIM_BOX_COUNT_[0]",
"KIMCHIB_BOX_COUNT_[0]",
"KIMCHI_BOX_COUNT_[8]"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00168041|10010022011713004101800031010000",
"x.com.samsung.da.description": "TP1X_REF_21K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
"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": "CO0",
"x.com.samsung.da.diagMinVersion": "3.0",
"x.com.samsung.da.diagTsId": "DA01",
"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": "250422",
"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": "24101006, 23031302, 24102200, 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": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "8",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Close"
}
]
}
},
{
"href": "/door/onedoorkimchi/vs/0",
"rep": {
"x.com.samsung.da.openState": "Close"
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.countryCode": "",
"x.com.samsung.da.region": ""
}
},
{
"href": "/drlc/0",
"rep": {
"DRLevel": 2,
"override": false
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "2",
"x.com.samsung.da.override": "Not_Supported",
"x.com.samsung.da.durationminutes": "1441",
"x.com.samsung.da.start": "2026-08-06T00:34:47Z",
"x.com.samsung.da.realSaving": "Off"
}
},
{
"href": "/bespoke/vs/0",
"rep": {
"x.com.samsung.da.BespokeProduct": "On"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "",
"otnList": [
{
"type": "WIFI",
"modelId": "A-RFWW-TP1-23-COMMON",
"versions": [
"20250422"
],
"visVersion": "250422"
},
{
"type": "Micom",
"modelId": "02120016804100168441",
"versions": [
"24101006",
"23031302"
],
"visVersion": "241010"
},
{
"type": "Micom",
"modelId": "021270659241FFFFFFFF",
"versions": [
"24102200",
"FFFFFFFF"
],
"visVersion": "241022"
}
]
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.ado.voicecontrol": "Off",
"x.com.samsung.da.device.sound": "On"
}
},
{
"href": "/status/kimchi/onedoor/vs/0",
"rep": {
"x.com.samsung.da.currentMode": "KIMCHI_STORAGE_NORMAL",
"x.com.samsung.da.ripeStatus": "Off",
"x.com.samsung.da.ripeRemaintime": "0",
"x.com.samsung.da.rackCount": "8",
"x.com.samsung.da.ripeTotaltime": "0",
"x.com.samsung.da.supportMode": [
"KIMCHI_STORAGE_NORMAL",
"KIMCHI_STORAGE_COLD",
"KIMCHI_STORAGE_WARM",
"KIMCHI_STORAGE_CRUNFCH",
"KIMCHI_STORAGE_BUY",
"STORAGE_FRIDGE",
"STORAGE_FREEZER",
"KIMCHI_RIPE_NORMAL_TEMP",
"KIMCHI_RIPE_LOW_TEMP",
"NEWMODE_KIMCHI_0000",
"NEWMODE_KIMCHI_0000",
"NEWMODE_KIMCHI_0000"
]
}
},
{
"href": "/autodoor/timer/vs/0",
"rep": {
"x.com.samsung.da.time.desired": "1",
"x.com.samsung.da.time.supportedOptions": [
"1",
"2",
"3",
"4",
"5",
"6"
]
}
},
{
"href": "/autodoor/kimchi/vs/0",
"rep": {
"x.com.samsung.da.ado.openOptions": [
"Single"
]
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"ble_ocf",
null
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
}
]
}
+466
View File
@@ -0,0 +1,466 @@
{
"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": "/alarms/vs/0",
"rt": [
"x.com.samsung.da.alarms"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/autodoor/winecellar/vs/0",
"rep": {
"x.com.samsung.da.ado.openOptions": [
"Single"
],
"href": "/autodoor/winecellar/vs/0"
}
},
{
"href": "/autodoor/timer/vs/0",
"rep": {
"x.com.samsung.da.time.desired": "1",
"x.com.samsung.da.time.supportedOptions": [
"1",
"2",
"3",
"4",
"5",
"6"
],
"href": "/autodoor/timer/vs/0"
}
},
{
"href": "/bespoke/vs/0",
"rep": {
"x.com.samsung.da.BespokeProduct": "On",
"href": "/bespoke/vs/0"
}
},
{
"href": "/cabinet/light/total/vs/0",
"rep": {
"x.com.samsung.da.lightLevel": "100",
"x.com.samsung.da.lightResolution": "1",
"x.com.samsung.da.lightControl.off.include": "Off",
"x.com.samsung.da.lightControl": "Off",
"x.com.samsung.da.timeout.desired": "0",
"x.com.samsung.da.timeout.supportedList": [
"0",
"15",
"30",
"60"
],
"href": "/cabinet/light/total/vs/0",
"rt": [
"x.com.samsung.da.cabinetlight"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.region": "",
"x.com.samsung.da.countryCode": "",
"href": "/configuration/vs/0"
}
},
{
"href": "/door/winecellar/vs/0",
"rep": {
"x.com.samsung.da.openState": "Close",
"href": "/door/winecellar/vs/0",
"rt": [
"x.com.samsung.da.doorwinecellar"
],
"if": [
"oic.if.baseline",
"oic.if.s"
]
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.openState": "Close",
"x.com.samsung.da.id": "9",
"x.com.samsung.da.description": "Door"
}
],
"href": "/doors/vs/0"
}
},
{
"href": "/drlc/vs/0",
"rep": {
"x.com.samsung.da.drlcLevel": "2",
"x.com.samsung.da.override": "Not_Supported",
"x.com.samsung.da.durationminutes": "1345",
"x.com.samsung.da.start": "2026-08-06T02:08:48Z",
"x.com.samsung.da.realSaving": "On",
"href": "/drlc/vs/0"
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.cumulativeConsumption": "19073",
"x.com.samsung.da.instantaneousPower": "5",
"x.com.samsung.da.cumulativePower": "210483",
"x.com.samsung.da.cumulativeSavedPower": "15696",
"x.com.samsung.da.cumulativeUnit": "Wh",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"href": "/energy/consumption/vs/0"
}
},
{
"href": "/file/information/vs/0",
"rep": {
"x.com.samsung.timeoffset": "+00:00",
"x.com.samsung.supprtedtype": 1,
"href": "/file/information/vs/0"
}
},
{
"href": "/filter/deodorfilter/vs/0",
"rep": {
"x.com.samsung.da.filterUsage": "-1",
"x.com.samsung.da.filterUsageResolution": "1",
"x.com.samsung.da.filterResetType": [
"replaceable"
],
"x.com.samsung.da.filterStatus": "normal",
"href": "/filter/deodorfilter/vs/0"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP1X_REF_21K|00146141|000B0020001513924100000011000000",
"x.com.samsung.da.description": "TP1X_REF_21K",
"x.com.samsung.da.serialNum": "**REDACTED**",
"x.com.samsung.da.otnDUID": "**REDACTED**",
"x.com.samsung.da.diagDumpType": "file",
"x.com.samsung.da.diagEndPoint": "SSM",
"x.com.samsung.da.diagLogType": [
"errCode",
"dump"
],
"x.com.samsung.da.diagMnid": "0AJT",
"x.com.samsung.da.diagSetupid": "531",
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"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": "260619",
"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": "24102313, 2201030E",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"href": "/information/vs/0"
}
},
{
"href": "/status/lock/vs/0",
"rep": {
"x.com.samsung.da.ado.voicecontrol": "Off",
"x.com.samsung.da.ado.soundcontrol": "On",
"href": "/status/lock/vs/0",
"rt": [
"x.com.samsung.da.lockstatus"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.modes": [
"AIRFILTER_DISABLE"
],
"x.com.samsung.da.supportedModes": [
"HOMECARE_WIZARD_V2",
"ENERGY_REPORT_MODEL",
"WINE_T_TOTRACK_[6]",
"WINE_T_BOTTLE_RACK_[7]",
"WINE_B_TOTRACK_[4]",
"WINE_B_BOTTLE_RACK_[7]",
"WINE_PRESENTATION_[6]",
"WINE_BTM_BOTTLE_[20]",
"WINE_PANTRY_BOTTLE_[5]"
],
"href": "/mode/vs/0",
"rt": [
"x.com.samsung.da.mode"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/realtimenotiforclient/vs/0",
"rep": {
"x.com.samsung.da.timeforshortnoti": "0",
"x.com.samsung.da.periodicnotisubscription": "true",
"href": "/realtimenotiforclient/vs/0"
}
},
{
"href": "/runningmode/vs/0",
"rep": {
"x.com.samsung.da.runningMode": 0,
"href": "/runningmode/vs/0"
}
},
{
"href": "/selfcheck/vs/0",
"rep": {
"x.com.samsung.da.supportedActions": [
"Start"
],
"x.com.samsung.da.status": "Ready",
"x.com.samsung.da.result": "Success",
"x.com.samsung.da.error": [
"ErrorCode_None"
],
"href": "/selfcheck/vs/0"
}
},
{
"href": "/temperature/desired/winecellar/top/vs/0",
"rep": {
"temperature": 13.0,
"range": [
4.0,
18.0
],
"units": "C",
"href": "/temperature/desired/winecellar/top/vs/0",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperature/desired/winecellar/bottom/vs/0",
"rep": {
"temperature": 7.0,
"range": [
4.0,
18.0
],
"units": "C",
"href": "/temperature/desired/winecellar/bottom/vs/0",
"rt": [
"oic.r.temperature"
],
"if": [
"oic.if.baseline",
"oic.if.a"
]
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "2",
"x.com.samsung.da.description": "WineCellar-top",
"x.com.samsung.da.desired": "13",
"x.com.samsung.da.current": "13",
"x.com.samsung.da.maximum": "18",
"x.com.samsung.da.minimum": "4",
"x.com.samsung.da.unit": "Celsius"
},
{
"x.com.samsung.da.id": "3",
"x.com.samsung.da.description": "WineCellar-bottom",
"x.com.samsung.da.desired": "7",
"x.com.samsung.da.current": "7",
"x.com.samsung.da.maximum": "18",
"x.com.samsung.da.minimum": "4",
"x.com.samsung.da.unit": "Celsius"
}
],
"href": "/temperatures/vs/0"
}
},
{
"href": "/information/winecellar/vs/0",
"rep": {
"x.com.samsung.da.tbl.revision": "1",
"href": "/information/winecellar/vs/0"
}
},
{
"href": "/status/winecellar/pantry/one/vs/0",
"rep": {
"x.com.samsung.da.mode": "Fruit",
"x.com.samsung.range": [
"4",
"13"
],
"x.com.samsung.da.room": "0x10",
"x.com.samsung.da.name": "MULTI_PANTRY",
"x.com.samsung.da.unit": "Celsius",
"x.com.samsung.da.supportedOptions": [
"Processed_Meat",
"Cheese",
"Nuts",
"Fruit",
"Wine"
],
"href": "/status/winecellar/pantry/one/vs/0"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false",
"otnStatus": "None",
"flashingProgress": "0",
"otnCompleteDate": "noHistory",
"scheduledTime": "None",
"swVersionInfo": {
"platform": "Tizen Lite",
"oneUiVersion": "7.0 Refrigerator",
"osVersion": "4.0"
},
"otnList": [
{
"type": "WIFI",
"modelId": "A-RFWW-TP1-24-T4-RE2",
"versions": [
"20260619"
],
"visVersion": "260619"
},
{
"type": "Micom",
"modelId": "02900014614100146441",
"versions": [
"24102313",
"2201030E"
],
"visVersion": "241023"
}
]
}
},
{
"href": "/timezone/vs/0",
"rep": {
"timezoneid": "Asia/Seoul",
"offset": "+09:00",
"DST": "OFF"
}
},
{
"href": "/connectionconfig/vs/0",
"rep": {
"autoReconnectionMinVersion": "1.0",
"autoReconnection": "true",
"autoReconnectionProtocolType": [
"helper_hotspot",
"ble_ocf"
],
"supportedWiFiAuthType": [
"OPEN",
"WEP",
"WPA-PSK",
"WPA2-PSK",
"SAE"
],
"supportedWiFiCryptoType": [
"TKIP",
"AES",
"WEP-64",
"WEP-128"
],
"supportedWiFiFreq": [
"2.4G"
],
"calmConnectionCare": {
"version": "1.0",
"role": [
"things"
]
}
}
},
{
"href": "/wirelessinfo/vs/0",
"rep": {
"macaddressWiFi": "**REDACTED**",
"macaddressBLE": "**REDACTED**",
"connectedApSsid": "Home_IoT"
}
},
{
"href": "/quickcontrol/info/vs/0",
"rep": {
"supportedVersion": "1.0"
}
},
{
"href": "/dginformation/vs/0",
"rep": {
"enrolmentstatus": "Unknown",
"devicestate": "Unknown",
"lockstatus": "Unknown",
"nextduedate": "",
"workingminutes": 0,
"paymentinfo": {
"emiplan": "Unknown",
"currency": "Unknown",
"totalemi": 0,
"totalemipaid": 0
}
}
}
]
}
+1 -1
View File
@@ -59,5 +59,5 @@ def test_air_quality_and_filter_sensors_present():
assert state["dust"] == 10
assert state["fine_dust"] == 9
assert state["odor"] == 1
assert state["hepa_filter_usage"] == 0
assert state["hepa_filter_usage"] == 30
assert state["hepa_filter_status"] == "normal"
+31 -13
View File
@@ -95,10 +95,13 @@ def test_power_and_convenient_folded_into_climate():
assert "convenient_mode" not in state
def test_air_filter_usage_is_percentage_of_capacity():
"""filterUsage is a raw count in the capacity unit (100 of 500), surfaced as
a percentage rather than the misleading raw value."""
assert _state()["air_filter_usage"] == 20
def test_air_filter_usage_is_already_a_percentage():
"""filterUsage is already 0-100, not a raw count to divide by
filterCapacity (issue #330): this fixture (ARTIK051_PRAC_20K) reports
filterStatus == 'wash' at filterUsage == '100', which only holds if
filterUsage is already a percent -- dividing by the 500 capacity again
would read a filter due for washing as 20% fresh."""
assert _state()["air_filter_usage"] == 100
def test_climate_write_targets():
@@ -771,19 +774,34 @@ def test_tropical_night_state_levels_across_fixtures():
assert level("airconditioner_tp2x_rac_20k") == 16
def test_air_filter_usage_hours_reads_raw_count():
"""filterUsage is a lifetime hour counter (41 of 500) that resets on
filter replacement -- total_increasing, not measurement. Unit comes from
def test_air_filter_usage_hours_derived_from_percent_and_capacity():
"""filterUsage is a percent, not an hour count (issue #330): the lifetime
hour reading is derived from percent * capacity rather than read off
filterUsage directly. total_increasing, not measurement; unit comes from
filterCapacityUnit via unit_fn, not a hardcoded 'h'."""
desc = next(
e
for e in airconditioner.AIR_FILTER.entities
if e.key == "air_filter_usage_hours" and isinstance(e, SensorDesc)
)
assert desc.value_fn("41") == 41
assert desc.value_fn(41) == 41
assert desc.value_fn(None) is None
assert desc.value_fn("not-a-number") is None
assert desc.rep_fn is not None
assert (
desc.rep_fn(
{"x.com.samsung.da.filterUsage": "91", "x.com.samsung.da.filterCapacity": "500"}
)
== 455.0
)
assert (
desc.rep_fn(
{
"x.com.samsung.da.filterUsage": "not-a-number",
"x.com.samsung.da.filterCapacity": "500",
}
)
is None
)
assert desc.rep_fn({"x.com.samsung.da.filterCapacity": "500"}) is None
assert desc.rep_fn({"x.com.samsung.da.filterUsage": "91"}) is None
assert desc.device_class == "duration"
assert desc.state_class == "total_increasing"
assert desc.unit_fn is not None
@@ -830,8 +848,8 @@ def test_air_filter_threshold_absent_without_supported_enum():
reg, resources = _ac_windfree()
state = flatten(discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
assert "air_filter_threshold" not in state
assert state["air_filter_usage_hours"] == 41
assert state["air_filter_usage"] == 8 # 41/500 -> 8%
assert state["air_filter_usage"] == 41
assert state["air_filter_usage_hours"] == 205.0 # 41% of 500
def test_air_filter_threshold_binds_on_enum_board():
+79 -2
View File
@@ -1132,12 +1132,16 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora():
)
def _new_subdevice_aware_state_keys(name):
def _new_subdevice_aware_state_keys(name, device_types=()):
"""Like _new_state_keys, but runs the full subdevice-aware pipeline
(enumerate_subdevices + discover_partitioned, issue #177) instead of a
single discover() call, so the golden for a composite-device fixture
captures every materialized subdevice's keys
(subdevice1_-/subdevice_<uuid>_-prefixed), not just the master's."""
(subdevice1_-/subdevice_<uuid>_-prefixed), not just the master's.
`device_types` threads through to discover_partitioned's own
`oic_device_types` -- needed for a board with no /information/vs/0 at
all to route from (issue #324's range)."""
from custom_components.localthings.registry.adapter import flatten
from tests.conftest import _discover_full, _load_device_full
@@ -1146,6 +1150,7 @@ def _new_subdevice_aware_state_keys(name):
resources,
oic_res,
seeds,
device_types,
)
state = flatten(bound, full_resources)
return sorted(state.keys())
@@ -1407,6 +1412,78 @@ def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_def
)
def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_autodoor():
"""TP1X_REF_21K, single-freezer-door Auto Door Open variant (issue
#328): /autodoor/single/vs/0 + /autodoor/timer/vs/0, plus
STATUS_LOCK's ado.voicecontrol sibling toggle."""
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_autodoor")
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_autodoor.json").read_text())
state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_autodoor", 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_refrigerator_tp1x_ref_21k_kimchi():
"""Kimchi-refrigerator Auto Door Open variant (issue #328) -- resolves
via /oic/d's oic.d.krefrigerator, not modelNum (the board is the same
TP1X_REF_21K as the regular fridge, so only the OCF type tells them
apart before checking hrefs)."""
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_kimchi")
golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_kimchi.json").read_text())
state_keys = _new_state_keys(
"refrigerator_tp1x_ref_21k_kimchi",
resources,
device_types=("oic.wk.d", "oic.d.krefrigerator"),
)
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_refrigerator_winecellar():
"""Wine-cellar refrigerator variant (issue #328) -- resolves via /oic/d's
x.com.st.d.winecellar, same TP1X_REF_21K board as the other two. Adds
the deodorizing filter at its own href, the multi-compartment pantry
select, and the table-revision info resource."""
from tests.conftest import _load_device
resources = _load_device("refrigerator_winecellar")
golden = json.loads((GOLDEN / "refrigerator_winecellar.json").read_text())
state_keys = _new_state_keys(
"refrigerator_winecellar",
resources,
device_types=("oic.wk.d", "x.com.st.d.winecellar"),
)
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_range_tp1x_da_ks_range_0101x():
"""Dual-cavity range (issue #324): no /information/vs/0 at all, so
routing depends entirely on /oic/d's oic.d.range -- and the second
cavity is a genuine Pattern A indexed subdevice at /device/1."""
name = "range_tp1x_da_ks_range_0101x"
golden = json.loads((GOLDEN / f"{name}.json").read_text())
state_keys = _new_subdevice_aware_state_keys(name, device_types=("oic.wk.d", "oic.d.range"))
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_resources_from_batch_preferred_over_flat():
from tests.conftest import _resources_from_dump
@@ -0,0 +1,92 @@
"""Dual-cavity range (TP1X_DA-KS-RANGE-0101X, NE63T8751SG/AA-class, issue
#324): no /information/vs/0 at all, so the modelNum-based fallback has
nothing to read -- routing depends entirely on /oic/d's own device type
(`oic.d.range`), read separately from the /device/0 batch (see
registry/identity.py and by_type/__init__.py's `_OIC_TYPE_TO_KEY`).
The second oven cavity answers as an indexed sibling at /device/1 (Pattern
A, same mechanism as the AC family's issue #177 fixtures) -- the master's
/mode/vs/0 (defaultMode 'UpperConvectionBake') is the upper cavity, the
subdevice's canonical /mode/vs/0 (defaultMode 'LowerConvectionBake') is the
lower one.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
from tests.conftest import _discover_full, _load_device_full
FIXTURE = "range_tp1x_da_ks_range_0101x"
DEVICE_TYPES = ("oic.wk.d", "oic.d.range")
def _discover():
resources, oic_res, seeds = _load_device_full(FIXTURE)
return _discover_full(resources, oic_res, seeds, DEVICE_TYPES)
def test_resolves_via_oic_type():
reg = for_device_by_oic_type(DEVICE_TYPES)
assert reg is not None and reg.name == "range"
def test_resolves_via_the_full_resolve_entrypoint():
resources, _oic_res, _seeds = _load_device_full(FIXTURE)
reg = resolve(resources, device_types=DEVICE_TYPES)
assert reg is not None and reg.name == "range"
def test_no_unbound_hrefs():
"""Every resource in the issue #324 dump binds or is ignored, on both
the master (upper cavity) and the materialized second cavity -- clears
the coverage-gap repair."""
resources, oic_res, seeds = _load_device_full(FIXTURE)
unbound = []
from custom_components.localthings.registry.registry import CAPABILITIES
from custom_components.localthings.registry.subdevices import (
discover_partitioned,
enumerate_subdevices,
)
from tests.conftest import FakeCoapSession
sess = FakeCoapSession(seeds)
candidates, extra = enumerate_subdevices(sess, resources, oic_res)
full_resources = {**resources, **extra}
discover_partitioned(
full_resources,
candidates,
resolve,
CAPABILITIES,
log=unbound.append,
oic_device_types=DEVICE_TYPES,
)
assert unbound == []
def test_second_cavity_materializes_as_an_indexed_subdevice():
_bound, materialized, skipped, _full_resources, device_type_name = _discover()
assert device_type_name == "range"
assert skipped == []
assert [(s.kind, s.key) for s in materialized] == [("indexed", "1")]
assert materialized[0].seed_path == ("device", "1")
def test_both_cavities_expose_distinct_oven_state():
bound, _materialized, _skipped, full_resources, _name = _discover()
state = flatten(bound, full_resources)
for key in ("oven_mode", "oven_setpoint", "oven_state", "machine_state", "door_open"):
assert key in state, key
# Second cavity's entities carry the subdevice1_ prefix (adapter._key) --
# the master's own keys stay unprefixed, so both cavities get distinct
# unique_ids rather than colliding on the same entity key.
assert "subdevice1_oven_mode" in state
assert "subdevice1_oven_setpoint" in state
def test_cooktop_monitoring_present_on_the_master_only():
"""The cooktop half belongs to the appliance as a whole, not either oven
cavity -- /cooktopmonitoring/vs/0 has no per-cavity index."""
bound, _materialized, _skipped, full_resources, _name = _discover()
state = flatten(bound, full_resources)
assert "cooktop_running_state" in state
assert "warming_center_state" in state
assert "subdevice1_cooktop_running_state" not in state
@@ -0,0 +1,80 @@
"""Auto Door Open (issue #328, TP1X_REF_21K single-freezer-door variant).
`/status/lock/vs/0`'s ado.devicecontrol switch was already modeled
(STATUS_LOCK.auto_door_opener); this dump adds the paired voice-feedback
toggle, the door-hold timer, and the per-variant capability declaration
href -- see fridge.py's AUTO_DOOR_TIMER/AUTO_DOOR_VARIANT comments.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.capabilities import fridge
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc
from tests.conftest import _load_device
FIXTURE = "refrigerator_tp1x_ref_21k_autodoor"
def _fridge():
resources = _load_device(FIXTURE)
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 _state():
reg, resources = _fridge()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_to_refrigerator_registry():
reg, _ = _fridge()
assert reg is not None and reg.name == "refrigerator"
def test_no_unbound_hrefs():
reg, resources = _fridge()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_auto_door_voice_control_reads_status_lock():
state = _state()
assert state["auto_door_opener"] is False
assert state["auto_door_voice_control"] is False
# This variant's dump has no ado.soundcontrol field -- gated absent.
assert "auto_door_sound_control" not in state
def test_auto_door_timer_reads_its_own_options():
desc = next(
e
for e in fridge.AUTO_DOOR_TIMER.entities
if e.key == "auto_door_timer" and isinstance(e, SelectDesc)
)
rep = {
"x.com.samsung.da.time.desired": "1",
"x.com.samsung.da.time.supportedOptions": ["1", "2", "3", "4", "5", "6"],
}
assert desc.value_fn(rep["x.com.samsung.da.time.desired"]) == "1"
assert desc.options_field == "x.com.samsung.da.time.supportedOptions"
assert desc.write_fn is not None
assert desc.write_fn("3", rep) == (
["autodoor", "timer", "vs", "0"],
{"x.com.samsung.da.time.desired": "3"},
)
def test_auto_door_single_variant_href_bound_with_no_entities():
"""/autodoor/single/vs/0 only ever declares openOptions=['Single'] --
no paired current/desired field to expose, so it's coverage-only via
the shared AUTO_DOOR_VARIANT pattern cap, not an entry of its own."""
assert fridge.AUTO_DOOR_VARIANT.entities == ()
assert fridge.AUTO_DOOR_VARIANT.match_fn is not None
_reg, resources = _fridge()
assert fridge.AUTO_DOOR_VARIANT.match_fn(resources["/autodoor/single/vs/0"], resources)
@@ -0,0 +1,57 @@
"""Kimchi-refrigerator Auto Door Open variant (issue #328,
oic.d.krefrigerator device type -- TP1X_REF_21K board, single kimchi
compartment). Same STATUS_LOCK/AUTO_DOOR_TIMER shape as the regular
TP1X_REF_21K, at the kimchi-specific /autodoor/kimchi/vs/0 declaration
href.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
from custom_components.localthings.registry.capabilities import fridge
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
FIXTURE = "refrigerator_tp1x_ref_21k_kimchi"
DEVICE_TYPES = ("oic.wk.d", "oic.d.krefrigerator")
def _fridge():
resources = _load_device(FIXTURE)
reg = resolve(resources, device_types=DEVICE_TYPES)
return reg, resources
def _state():
reg, resources = _fridge()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_via_oic_type():
reg = for_device_by_oic_type(DEVICE_TYPES)
assert reg is not None and reg.name == "refrigerator"
def test_resolves_to_refrigerator_registry():
reg, _ = _fridge()
assert reg is not None and reg.name == "refrigerator"
def test_no_unbound_hrefs():
reg, resources = _fridge()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_auto_door_opener_present():
state = _state()
assert "auto_door_opener" in state
assert "auto_door_timer" in state
def test_auto_door_kimchi_variant_href_bound_with_no_entities():
assert fridge.AUTO_DOOR_VARIANT.entities == ()
assert fridge.AUTO_DOOR_VARIANT.match_fn is not None
_reg, resources = _fridge()
assert fridge.AUTO_DOOR_VARIANT.match_fn(resources["/autodoor/kimchi/vs/0"], resources)
@@ -0,0 +1,90 @@
"""Wine-cellar refrigerator variant (issue #328, x.com.st.d.winecellar
device type -- RW33C99B1TFG-class, TP1X_REF_21K board). Auto Door Open's
winecellar-specific declaration href, a deodorizing filter reported at a
different href than the regular AIR_FILTER, a multi-compartment pantry
select, and a table-revision info resource -- all new coverage this dump
surfaced.
"""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve
from custom_components.localthings.registry.capabilities import fridge
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc
from tests.conftest import _load_device
FIXTURE = "refrigerator_winecellar"
DEVICE_TYPES = ("oic.wk.d", "x.com.st.d.winecellar")
def _fridge():
resources = _load_device(FIXTURE)
reg = resolve(resources, device_types=DEVICE_TYPES)
return reg, resources
def _state():
reg, resources = _fridge()
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
return flatten(bound, resources)
def test_resolves_via_oic_type():
reg = for_device_by_oic_type(DEVICE_TYPES)
assert reg is not None and reg.name == "refrigerator"
def test_no_unbound_hrefs():
reg, resources = _fridge()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_auto_door_sound_control_present_here_only():
"""This variant's dump is the only one that carries
ado.soundcontrol -- the other TP1X_REF_21K auto-door fixtures don't."""
state = _state()
assert state["auto_door_sound_control"] is True
assert state["auto_door_voice_control"] is False
def test_deodor_filter_has_its_own_keys_not_air_filters():
"""Same shape/reasoning as fridge.AIR_FILTER (percentage-not-raw-count,
see its own comment) but its own 'deodor_'-prefixed keys, not a shared
tuple -- AIR_FILTER's own docstring picked 'air_' specifically to avoid
colliding with WATER_FILTER's filter_usage/filter_status, and reusing
AIR_FILTER.entities verbatim here would silently recreate that same
collision if a unit ever reports both hrefs."""
assert fridge.DEODOR_FILTER.entities != fridge.AIR_FILTER.entities
state = _state()
assert state["deodor_filter_status"] == "normal"
# The device's own sentinel, relayed as-is -- not a real 0-100 reading
# on this unit, and no second dump to say what else -1 could mean.
assert state["deodor_filter_usage"] == -1
assert "air_filter_usage" not in state
assert "air_filter_status" not in state
def test_winecellar_pantry_zone_mode_options_and_write():
desc = next(
e
for e in fridge.WINECELLAR_PANTRY_ZONE.entities
if e.key == "winecellar_pantry_zone_mode" and isinstance(e, SelectDesc)
)
assert desc.options_field == "x.com.samsung.da.supportedOptions"
assert desc.write_fn is not None
assert desc.write_fn("Cheese", {}) == (
["status", "winecellar", "pantry", "one", "vs", "0"],
{"x.com.samsung.da.mode": "Cheese"},
)
state = _state()
assert state["winecellar_pantry_zone_mode"] == "Fruit"
def test_winecellar_variant_and_info_hrefs_bound_with_no_entities():
assert fridge.AUTO_DOOR_VARIANT.entities == ()
assert fridge.WINECELLAR_INFO.entities == ()
assert fridge.AUTO_DOOR_VARIANT.match_fn is not None
_reg, resources = _fridge()
assert fridge.AUTO_DOOR_VARIANT.match_fn(resources["/autodoor/winecellar/vs/0"], resources)