diff --git a/custom_components/localthings/manifest.json b/custom_components/localthings/manifest.json index 1c39c15..ae994df 100644 --- a/custom_components/localthings/manifest.json +++ b/custom_components/localthings/manifest.json @@ -12,5 +12,5 @@ "pyOpenSSL>=23.0", "smartthings-local>=0.1.0" ], - "version": "0.7.0" + "version": "0.8.0" } diff --git a/custom_components/localthings/registry/by_type/__init__.py b/custom_components/localthings/registry/by_type/__init__.py index d9b358a..a8ae11f 100644 --- a/custom_components/localthings/registry/by_type/__init__.py +++ b/custom_components/localthings/registry/by_type/__init__.py @@ -69,6 +69,7 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = { 'WW': 'washer', 'WD': 'washer', 'WF': 'washer', + 'WV': 'washer', # FlexWash twin units (e.g. WV55M9600AW) -- issue #19 'DV': 'dryer', 'DW': 'dishwasher', } diff --git a/custom_components/localthings/registry/by_type/refrigerator.py b/custom_components/localthings/registry/by_type/refrigerator.py index 003eb8d..caddc51 100644 --- a/custom_components/localthings/registry/by_type/refrigerator.py +++ b/custom_components/localthings/registry/by_type/refrigerator.py @@ -1,5 +1,5 @@ """Refrigerator device registry.""" -from ..capabilities import common, fridge, ignored +from ..capabilities import common, dishwasher, fridge, ignored from ._base import DeviceRegistry, _build REGISTRY = DeviceRegistry( @@ -17,6 +17,7 @@ REGISTRY = DeviceRegistry( common.ALARMS, common.ENERGY_METER, common.WATER_FILTER, + dishwasher.DIAGNOSIS, fridge.ICEMAKER_NIGHTTIME, fridge.FLEX_ZONE, fridge.REFRIGERATION, @@ -26,6 +27,7 @@ REGISTRY = DeviceRegistry( fridge.CABINET_LIGHT_ENHANCED, fridge.SABBATH, fridge.BEVERAGE_ZONE, + fridge.PANTRY_ZONE, fridge.FIRMWARE_UPDATE, fridge.DEFROST_DELAY, fridge.DEFROST_DELAY_NATIVE_DUPLICATE, diff --git a/custom_components/localthings/registry/capabilities/common.py b/custom_components/localthings/registry/capabilities/common.py index eadd646..edc3f6c 100644 --- a/custom_components/localthings/registry/capabilities/common.py +++ b/custom_components/localthings/registry/capabilities/common.py @@ -175,6 +175,38 @@ ENERGY_METER = Capability( state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh, exists_fn=lambda rep, resources: ( not rep or 'x.com.samsung.da.cumulativePower' in rep)), + # cumulativeConsumption is a second, independently-varying running + # total alongside cumulativePower -- some fridges (issue #26) report + # both. Self-gates off where only cumulativePower is present. `not + # rep or` keeps the same empty-{} stub carve-out as power_watts/ + # energy_kwh above -- without it, an exists_fn permanently drops the + # entity if setup happens to land on a not-yet-fetched stub. + SensorDesc(key='power_energy_kwh', field='x.com.samsung.da.cumulativeConsumption', + name='Power energy', device_class='energy', + state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh, + exists_fn=lambda rep, resources: ( + not rep or 'x.com.samsung.da.cumulativeConsumption' in rep)), + # AI Energy Mode's lifetime savings estimate vs. an unoptimized + # baseline -- present on some models (e.g. TP1X_REF_21K, issue #21/ + # #27) and absent on others (issue #20/#26), unlike cumulativePower. + SensorDesc(key='energy_saved_kwh', field='x.com.samsung.da.cumulativeSavedPower', + name='Energy saved', device_class='energy', + state_class='total_increasing', unit='kWh', value_fn=wh_to_kwh, + exists_fn=lambda rep, resources: ( + not rep or 'x.com.samsung.da.cumulativeSavedPower' in rep)), + # Monthly billing-cycle totals -- the completed prior month and the + # in-progress current month. Not ever-increasing (each resets at + # month boundary), so no state_class. + SensorDesc(key='energy_last_month_kwh', field='x.com.samsung.da.monthlyConsumption', + name='Energy (last month)', device_class='energy', + unit='kWh', value_fn=wh_to_kwh, + exists_fn=lambda rep, resources: ( + not rep or 'x.com.samsung.da.monthlyConsumption' in rep)), + SensorDesc(key='energy_this_month_kwh', field='x.com.samsung.da.thismonthlyConsumption', + name='Energy (this month)', device_class='energy', + unit='kWh', value_fn=wh_to_kwh, + exists_fn=lambda rep, resources: ( + not rep or 'x.com.samsung.da.thismonthlyConsumption' in rep)), ), ) diff --git a/custom_components/localthings/registry/capabilities/fridge.py b/custom_components/localthings/registry/capabilities/fridge.py index c788fe1..a258fa2 100644 --- a/custom_components/localthings/registry/capabilities/fridge.py +++ b/custom_components/localthings/registry/capabilities/fridge.py @@ -502,17 +502,64 @@ BEVERAGE_ZONE = Capability( ), ) +# --------------------------------------------------------------------------- +# Pantry / Cool Select Zone -- a convertible compartment toggled between +# wine/deli/drinks temperature presets (issue #20). Same shape as +# BEVERAGE_ZONE (a controllable named sub-zone with a mode + supported-modes +# list) but a distinct resource/field set -- x.com.samsung.da.mode / +# x.com.samsung.da.supportedOptions on /status/pantry/one/vs/0, rather than +# roomDesiredMode/roomSupportedModes on /specialzone/one/vs/0. Only a "one" +# instance has been seen; not generalized to a pattern cap until a second +# instance turns up. +# --------------------------------------------------------------------------- + +def _pantry_write(p, rep, href=None): + return ['status', 'pantry', 'one', 'vs', '0'], {'x.com.samsung.da.mode': p} + + +PANTRY_ZONE = Capability( + href='/status/pantry/one/vs/0', + poll_tier='warm', + entities=( + SelectDesc(key='pantry_zone_mode', field='x.com.samsung.da.mode', + name='Pantry zone mode', icon='mdi:glass-wine', + translation_key='pantry_zone_mode', + entity_category='config', + options_field='x.com.samsung.da.supportedOptions', + write_fn=_pantry_write), + ), +) + # --------------------------------------------------------------------------- # Flex zone (convertible drawer — /mode/vs/0 on RF9000-class fridges) # -# x.com.samsung.da.modes holds multiple orthogonal flags in one list. -# The flex zone entry is identified by the CV_TTYPE_RF9000A_ prefix. +# x.com.samsung.da.modes holds multiple orthogonal flags in one list; the +# flex-zone entry is whichever item is also a member of supportedOptions -- +# the other flags (WATERFILTER_*, DEFROST_BLOCK_*, the CVN_*_ZONE marker) +# never appear there. The prefix on that item varies by fridge family +# (CV_TTYPE_RF9000A_ on RF9000-class, CV_FDR_ on Bespoke-class -- issue #27 / +# #26, where the old CV_TTYPE_RF9000A_-only match left this entity bound but +# stuck on None), so match by list membership instead of a hardcoded prefix. # Write replaces only that item; other flags are preserved. # --------------------------------------------------------------------------- +def _flex_zone_supported(rep): + return set(rep.get('x.com.samsung.da.supportedOptions') or ()) + + +def _flex_zone_current(rep): + # Every dump seen has at most one modes/supportedOptions overlap, so + # "first match" and "strip all matches" (in the write below) agree. If a + # future device ever reports two, this reads the first and the write + # would drop both -- revisit if that turns up. + modes = rep.get('x.com.samsung.da.modes') or [] + supported = _flex_zone_supported(rep) + return next((m for m in modes if m in supported), None) + + def _flex_zone_write(p, rep, href=None): - modes = list(rep.get('x.com.samsung.da.modes') or []) - modes = [m for m in modes if not m.startswith('CV_TTYPE_RF9000A_')] + supported = _flex_zone_supported(rep) + modes = [m for m in (rep.get('x.com.samsung.da.modes') or []) if m not in supported] modes.append(p) return ['mode', 'vs', '0'], {'x.com.samsung.da.modes': modes} @@ -522,16 +569,13 @@ FLEX_ZONE = Capability( poll_tier='warm', entities=( SelectDesc(key='flex_zone_mode', - field='x.com.samsung.da.modes', name='Flex zone mode', icon='mdi:thermostat', translation_key='flex_zone_mode', entity_category='config', options_field='x.com.samsung.da.supportedOptions', exists_fn=lambda rep, resources: bool( rep.get('x.com.samsung.da.supportedOptions')), - value_fn=lambda modes: next( - (m for m in (modes or []) - if m.startswith('CV_TTYPE_RF9000A_')), None), + rep_fn=_flex_zone_current, write_fn=_flex_zone_write), ), ) diff --git a/custom_components/localthings/registry/capabilities/washer.py b/custom_components/localthings/registry/capabilities/washer.py index 14d3893..964a26c 100644 --- a/custom_components/localthings/registry/capabilities/washer.py +++ b/custom_components/localthings/registry/capabilities/washer.py @@ -82,6 +82,19 @@ WASHER_SETTINGS = Capability( options_field='x.com.samsung.da.supportedRinseCycles', write_fn=lambda p, rep, href=None: ( ['washer', 'vs', '0'], {'x.com.samsung.da.rinseCycles': p})), + # Washer/dryer combo units carry a dryLevel field on the wash + # resource itself (no separate dryer device/course) -- see issue + # #22. Self-gates off on plain washers, which never report + # supportedDryLevel. + SelectDesc(key='dry_level', field='x.com.samsung.da.dryLevel', + name='Dry level', icon='mdi:tumble-dryer', + entity_category='config', + translation_key='washer_dry_level', + options_field='x.com.samsung.da.supportedDryLevel', + exists_fn=lambda rep, resources: bool( + rep.get('x.com.samsung.da.supportedDryLevel')), + write_fn=lambda p, rep, href=None: ( + ['washer', 'vs', '0'], {'x.com.samsung.da.dryLevel': p})), ), ) diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json index 2647ab0..2b2eca4 100644 --- a/custom_components/localthings/strings.json +++ b/custom_components/localthings/strings.json @@ -30,13 +30,25 @@ "sp_ttype_wine_dessert": "Wine and Dessert" } }, + "pantry_zone_mode": { + "state": { + "fdr_wine": "Wine", + "fdr_deli": "Deli", + "fdr_drinks": "Drinks" + } + }, "flex_zone_mode": { "state": { "cv_ttype_rf9000a_freeze": "Freeze", "cv_ttype_rf9000a_softfreeze": "Soft Freeze", "cv_ttype_rf9000a_meat_fish": "Meat/Fish", "cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies", - "cv_ttype_rf9000a_beverage": "Beverage" + "cv_ttype_rf9000a_beverage": "Beverage", + "cv_fdr_wine": "Wine", + "cv_fdr_deli": "Deli", + "cv_fdr_beverage": "Beverage", + "cv_fdr_meat": "Meat", + "cv_fdr_soft_freezer": "Soft Freezer" } }, "dishwasher_cycle": { @@ -119,6 +131,18 @@ "02": "2x", "03": "3x" } + }, + "washer_dry_level": { + "state": { + "none": "Off", + "cupboard": "Cupboard", + "30": "30 min", + "60": "1 hr", + "90": "1 hr 30", + "120": "2 hr", + "180": "3 hr", + "240": "4 hr" + } } }, "sensor": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 2647ab0..2b2eca4 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -30,13 +30,25 @@ "sp_ttype_wine_dessert": "Wine and Dessert" } }, + "pantry_zone_mode": { + "state": { + "fdr_wine": "Wine", + "fdr_deli": "Deli", + "fdr_drinks": "Drinks" + } + }, "flex_zone_mode": { "state": { "cv_ttype_rf9000a_freeze": "Freeze", "cv_ttype_rf9000a_softfreeze": "Soft Freeze", "cv_ttype_rf9000a_meat_fish": "Meat/Fish", "cv_ttype_rf9000a_fruit_veggies": "Fruit & Veggies", - "cv_ttype_rf9000a_beverage": "Beverage" + "cv_ttype_rf9000a_beverage": "Beverage", + "cv_fdr_wine": "Wine", + "cv_fdr_deli": "Deli", + "cv_fdr_beverage": "Beverage", + "cv_fdr_meat": "Meat", + "cv_fdr_soft_freezer": "Soft Freezer" } }, "dishwasher_cycle": { @@ -119,6 +131,18 @@ "02": "2x", "03": "3x" } + }, + "washer_dry_level": { + "state": { + "none": "Off", + "cupboard": "Cupboard", + "30": "30 min", + "60": "1 hr", + "90": "1 hr 30", + "120": "2 hr", + "180": "3 hr", + "240": "4 hr" + } } }, "sensor": { diff --git a/tests/fixtures/golden/airconditioner.json b/tests/fixtures/golden/airconditioner.json index afe8a6d..68abd0f 100644 --- a/tests/fixtures/golden/airconditioner.json +++ b/tests/fixtures/golden/airconditioner.json @@ -8,6 +8,7 @@ "climate", "diagnosis_status", "energy_kwh", + "energy_saved_kwh", "power_watts" ] } diff --git a/tests/fixtures/golden/dishwasher.json b/tests/fixtures/golden/dishwasher.json index 21ae55f..2a50531 100644 --- a/tests/fixtures/golden/dishwasher.json +++ b/tests/fixtures/golden/dishwasher.json @@ -8,6 +8,7 @@ "delay_start_hours", "diagnosis_status", "energy_kwh", + "energy_saved_kwh", "finish_time", "firmware_update", "heated_dry", @@ -39,6 +40,7 @@ "samsung_dishwasher_diagnosis_start", "samsung_dishwasher_diagnosis_status", "samsung_dishwasher_energy_kwh", + "samsung_dishwasher_energy_saved_kwh", "samsung_dishwasher_finish_time", "samsung_dishwasher_firmware_update", "samsung_dishwasher_heated_dry", diff --git a/tests/fixtures/golden/refrigerator.json b/tests/fixtures/golden/refrigerator.json index 96e4ac2..45ca68b 100644 --- a/tests/fixtures/golden/refrigerator.json +++ b/tests/fixtures/golden/refrigerator.json @@ -16,6 +16,9 @@ "door_cooler_open", "door_freezer_open", "energy_kwh", + "energy_last_month_kwh", + "energy_saved_kwh", + "energy_this_month_kwh", "filter_status", "filter_usage", "firmware_update", @@ -31,6 +34,7 @@ "icemaker_two_type", "night_end", "night_start", + "power_energy_kwh", "power_watts", "rapid_freezing", "rapid_fridge", @@ -56,6 +60,9 @@ "samsung_refrigerator_door_cooler_open", "samsung_refrigerator_door_freezer_open", "samsung_refrigerator_energy_kwh", + "samsung_refrigerator_energy_last_month_kwh", + "samsung_refrigerator_energy_saved_kwh", + "samsung_refrigerator_energy_this_month_kwh", "samsung_refrigerator_filter_status", "samsung_refrigerator_filter_usage", "samsung_refrigerator_firmware_update", @@ -71,6 +78,7 @@ "samsung_refrigerator_icemaker_two_type", "samsung_refrigerator_night_end", "samsung_refrigerator_night_start", + "samsung_refrigerator_power_energy_kwh", "samsung_refrigerator_power_watts", "samsung_refrigerator_rapid_freezing", "samsung_refrigerator_rapid_fridge", diff --git a/tests/fixtures/golden/refrigerator_artik051_ref_17k.json b/tests/fixtures/golden/refrigerator_artik051_ref_17k.json new file mode 100644 index 0000000..0a69dbb --- /dev/null +++ b/tests/fixtures/golden/refrigerator_artik051_ref_17k.json @@ -0,0 +1,28 @@ +{ + "state_keys": [ + "alarm_code", + "autofill", + "cooler_setpoint", + "cooler_temperature", + "defrost_active", + "defrost_delay", + "diagnosis_status", + "door_cooler_open", + "door_freezer_open", + "energy_last_month_kwh", + "energy_this_month_kwh", + "filter_status", + "filter_usage", + "firmware_update", + "freezer_setpoint", + "freezer_temperature", + "icemaker_one_enabled", + "icemaker_one_making_status", + "pantry_zone_mode", + "power_energy_kwh", + "power_watts", + "rapid_freezing", + "rapid_fridge", + "sabbath_mode" + ] +} diff --git a/tests/fixtures/golden/refrigerator_tp1x_ref_21k_us.json b/tests/fixtures/golden/refrigerator_tp1x_ref_21k_us.json index 962e01c..dd94692 100644 --- a/tests/fixtures/golden/refrigerator_tp1x_ref_21k_us.json +++ b/tests/fixtures/golden/refrigerator_tp1x_ref_21k_us.json @@ -10,6 +10,9 @@ "door_cvroom_open", "door_freezer_open", "energy_kwh", + "energy_last_month_kwh", + "energy_saved_kwh", + "energy_this_month_kwh", "filter_status", "filter_usage", "firmware_update", @@ -20,6 +23,7 @@ "icemaker_one_making_status", "icemaker_two_enabled", "icemaker_two_making_status", + "power_energy_kwh", "power_watts", "rapid_freezing", "rapid_fridge", diff --git a/tests/fixtures/golden/refrigerator_tp2x_ref_20k.json b/tests/fixtures/golden/refrigerator_tp2x_ref_20k.json new file mode 100644 index 0000000..ba43462 --- /dev/null +++ b/tests/fixtures/golden/refrigerator_tp2x_ref_20k.json @@ -0,0 +1,29 @@ +{ + "state_keys": [ + "alarm_code", + "cooler_setpoint", + "cooler_temperature", + "defrost_active", + "defrost_delay", + "diagnosis_status", + "door_cooler_open", + "door_cvroom_open", + "door_freezer_open", + "energy_kwh", + "energy_last_month_kwh", + "energy_this_month_kwh", + "filter_status", + "filter_usage", + "firmware_update", + "flex_zone_mode", + "freezer_setpoint", + "freezer_temperature", + "icemaker_one_enabled", + "icemaker_one_making_status", + "power_energy_kwh", + "power_watts", + "rapid_freezing", + "rapid_fridge", + "sabbath_mode" + ] +} diff --git a/tests/fixtures/golden/washer.json b/tests/fixtures/golden/washer.json index e8beedc..69ac2a8 100644 --- a/tests/fixtures/golden/washer.json +++ b/tests/fixtures/golden/washer.json @@ -11,6 +11,7 @@ "drum_clean_cycles_remaining", "drum_clean_last_cleaned", "energy_kwh", + "energy_saved_kwh", "finish_time", "firmware_update", "job_beginning_status", diff --git a/tests/fixtures/golden/washer_dryer_combo.json b/tests/fixtures/golden/washer_dryer_combo.json new file mode 100644 index 0000000..a228067 --- /dev/null +++ b/tests/fixtures/golden/washer_dryer_combo.json @@ -0,0 +1,26 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cycle", + "cycle_active", + "delay_start_hours", + "detergent_low", + "diagnosis_status", + "drum_clean_cycles_remaining", + "dry_level", + "energy_kwh", + "finish_time", + "firmware_update", + "job_beginning_status", + "machine_state", + "power_switch", + "progress", + "progress_percentage", + "remote_control", + "rinse_cycles", + "softener_low", + "spin_speed", + "wash_temperature" + ] +} diff --git a/tests/fixtures/golden/washer_flexwash.json b/tests/fixtures/golden/washer_flexwash.json new file mode 100644 index 0000000..6db1e00 --- /dev/null +++ b/tests/fixtures/golden/washer_flexwash.json @@ -0,0 +1,22 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cycle_active", + "delay_start_hours", + "detergent_low", + "diagnosis_status", + "drum_clean_cycles_remaining", + "energy_kwh", + "finish_time", + "job_beginning_status", + "machine_state", + "power_switch", + "progress", + "progress_percentage", + "remote_control", + "rinse_cycles", + "spin_speed", + "wash_temperature" + ] +} diff --git a/tests/fixtures/refrigerator_artik051_ref_17k_device.json b/tests/fixtures/refrigerator_artik051_ref_17k_device.json new file mode 100644 index 0000000..49e6069 --- /dev/null +++ b/tests/fixtures/refrigerator_artik051_ref_17k_device.json @@ -0,0 +1,351 @@ +{ + "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": "-21", + "x.com.samsung.da.current": "-21", + "x.com.samsung.da.maximum": "-15", + "x.com.samsung.da.minimum": "-23", + "x.com.samsung.da.unit": "Celsius" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Fridge", + "x.com.samsung.da.desired": "2", + "x.com.samsung.da.current": "2", + "x.com.samsung.da.maximum": "7", + "x.com.samsung.da.minimum": "1", + "x.com.samsung.da.unit": "Celsius" + } + ] + } + }, + { + "href": "/temperature/current/freezer/0", + "rep": { + "range": [ + -23.0, + -15.0 + ], + "units": "C", + "temperature": -21.0 + } + }, + { + "href": "/temperature/desired/freezer/0", + "rep": { + "range": [ + -23.0, + -15.0 + ], + "units": "C", + "temperature": -21.0 + } + }, + { + "href": "/temperature/current/cooler/0", + "rep": { + "range": [ + 1.0, + 7.0 + ], + "units": "C", + "temperature": 2.0 + } + }, + { + "href": "/temperature/desired/cooler/0", + "rep": { + "range": [ + 1.0, + 7.0 + ], + "units": "C", + "temperature": 2.0 + } + }, + { + "href": "/diagnosis/vs/0", + "rep": { + "x.com.samsung.da.diagnosisStart": "Ready" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeConsumption": "0", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPower": "0", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.monthlyConsumption": "86000", + "x.com.samsung.da.thismonthlyConsumption": "64026" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2", + "SUPPORT_SABBATH_CONTROL" + ], + "x.com.samsung.da.modes": [ + "WATERFILTER_ENABLE" + ] + } + }, + { + "href": "/mode/0", + "rep": { + "supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2", + "SUPPORT_SABBATH_CONTROL" + ], + "modes": [ + "WATERFILTER_ENABLE" + ] + } + }, + { + "href": "/defrost/block/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "DEFROST_BLOCK_ON", + "DEFROST_BLOCK_OFF" + ], + "x.com.samsung.da.modes": [ + "DEFROST_BLOCK_OFF" + ] + } + }, + { + "href": "/sabbath/vs/0", + "rep": { + "x.com.samsung.da.sabbathMode": "Off" + } + }, + { + "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": "ARTIK051_REF_17K|00117141|0002074E051311200103000010000000", + "x.com.samsung.da.description": "ARTIK051_REF_17K", + "x.com.samsung.da.serialNum": "REDACTED", + "x.com.samsung.da.otnDUID": "REDACTED", + "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": "02144A201124", + "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": "2003030C,FFFFFFFF", + "x.com.samsung.da.newVersionAvailable": "0" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "-04:00", + "x.com.samsung.supprtedtype": 1 + } + }, + { + "href": "/doors/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + } + ] + } + }, + { + "href": "/door/freezer/0", + "rep": { + "openState": "Close" + } + }, + { + "href": "/door/cooler/0", + "rep": { + "openState": "Close" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.countryCode": "US", + "x.com.samsung.da.region": "" + } + }, + { + "href": "/defrost/delay/0", + "rep": { + "value": false + } + }, + { + "href": "/defrost/delay/vs/0", + "rep": { + "x.com.samsung.da.delayDefrost": "Off" + } + }, + { + "href": "/defrost/reservation/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Summer Season", + "x.com.samsung.da.startTime": "0000-05-01T15:00:00", + "x.com.samsung.da.period": "04:00:00", + "x.com.samsung.da.endTime": "0000-10-31T00:00:00" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Winter Season", + "x.com.samsung.da.startTime": "0000-11-01T06:00:00", + "x.com.samsung.da.period": "04:00:00", + "x.com.samsung.da.endTime": "0000-04-30T00:00:00" + } + ] + } + }, + { + "href": "/icemaker/status/0", + "rep": { + "status": "On" + } + }, + { + "href": "/icemaker/status/vs/0", + "rep": { + "x.com.samsung.da.iceMaker": "On" + } + }, + { + "href": "/refrigeration/0", + "rep": { + "defrost": false, + "rapidFreeze": false, + "rapidCool": false + } + }, + { + "href": "/refrigeration/vs/0", + "rep": { + "x.com.samsung.da.rapidFridge": "Off", + "x.com.samsung.da.rapidFreezing": "Off" + } + }, + { + "href": "/drlc/0", + "rep": { + "DRLevel": 0, + "start": "00:00:00", + "duration": 0, + "override": false + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.drlcLevel": "0", + "x.com.samsung.da.duration": "00:00:00", + "x.com.samsung.da.drlcStartTime": "00:00:00", + "x.com.samsung.da.override": "Off" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "Micom", + "x.com.samsung.da.newVersionAvailable": "false" + } + }, + { + "href": "/filter/waterfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsage": "100", + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterStatus": "replace", + "x.com.samsung.da.filterResetType": [ + "replaceable" + ] + } + }, + { + "href": "/autofill/vs/0", + "rep": { + "x.com.samsung.da.autofill": "Off" + } + }, + { + "href": "/icemaker/one/vs/0", + "rep": { + "x.com.samsung.da.iceMaker.name": "ICE_MAKER", + "x.com.samsung.da.iceMaker.state": "On", + "x.com.samsung.da.iceMaker.type": "toggle", + "x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP", + "x.com.samsung.da.iceType.desired": "NORMAL" + } + }, + { + "href": "/status/pantry/one/vs/0", + "rep": { + "x.com.samsung.da.controllable": "true", + "x.com.samsung.da.name": "COOL_SELECT_ZONE", + "x.com.samsung.da.mode": "FDR_DRINKS", + "x.com.samsung.da.supportedOptions": [ + "FDR_WINE", + "FDR_DELI", + "FDR_DRINKS" + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/refrigerator_tp2x_ref_20k_device.json b/tests/fixtures/refrigerator_tp2x_ref_20k_device.json new file mode 100644 index 0000000..be906eb --- /dev/null +++ b/tests/fixtures/refrigerator_tp2x_ref_20k_device.json @@ -0,0 +1,351 @@ +{ + "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": "0", + "x.com.samsung.da.current": "0", + "x.com.samsung.da.maximum": "5", + "x.com.samsung.da.minimum": "-8", + "x.com.samsung.da.unit": "Fahrenheit" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Fridge", + "x.com.samsung.da.desired": "37", + "x.com.samsung.da.current": "37", + "x.com.samsung.da.maximum": "44", + "x.com.samsung.da.minimum": "34", + "x.com.samsung.da.unit": "Fahrenheit" + } + ] + } + }, + { + "href": "/temperature/current/freezer/0", + "rep": { + "range": [ + -8.0, + 5.0 + ], + "units": "F", + "temperature": 0.0 + } + }, + { + "href": "/temperature/desired/freezer/0", + "rep": { + "range": [ + -8.0, + 5.0 + ], + "units": "F", + "temperature": 0.0 + } + }, + { + "href": "/temperature/current/cooler/0", + "rep": { + "range": [ + 34.0, + 44.0 + ], + "units": "F", + "temperature": 37.0 + } + }, + { + "href": "/temperature/desired/cooler/0", + "rep": { + "range": [ + 34.0, + 44.0 + ], + "units": "F", + "temperature": 37.0 + } + }, + { + "href": "/diagnosis/vs/0", + "rep": { + "x.com.samsung.da.diagnosisStart": "Ready" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeConsumption": "288048", + "x.com.samsung.da.cumulativePower": "1797027", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPower": "115", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.monthlyConsumption": "55400", + "x.com.samsung.da.thismonthlyConsumption": "39737" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2", + "SUPPORT_SABBATH_CONTROL" + ], + "x.com.samsung.da.modes": [ + "CVN_CONVERTIBLE_ZONE", + "CV_FDR_BEVERAGE", + "WATERFILTER_DISABLE" + ], + "x.com.samsung.da.supportedOptions": [ + "CV_FDR_WINE", + "CV_FDR_DELI", + "CV_FDR_BEVERAGE", + "CV_FDR_MEAT" + ] + } + }, + { + "href": "/mode/0", + "rep": { + "supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2", + "SUPPORT_SABBATH_CONTROL" + ], + "modes": [ + "CVN_CONVERTIBLE_ZONE", + "CV_FDR_BEVERAGE", + "WATERFILTER_DISABLE" + ] + } + }, + { + "href": "/defrost/block/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "DEFROST_BLOCK_ON", + "DEFROST_BLOCK_OFF" + ], + "x.com.samsung.da.modes": [ + "DEFROST_BLOCK_OFF" + ] + } + }, + { + "href": "/sabbath/vs/0", + "rep": { + "x.com.samsung.da.sabbathMode": "Off" + } + }, + { + "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": "TP2X_REF_20K|00113141|0002033F051311200103000010000000", + "x.com.samsung.da.description": "TP2X_REF_20K", + "x.com.samsung.da.serialNum": "REDACTED", + "x.com.samsung.da.otnDUID": "REDACTED", + "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": "02144A220110", + "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": "2011030E,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": "0", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Door", + "x.com.samsung.da.openState": "Close" + } + ] + } + }, + { + "href": "/door/freezer/0", + "rep": { + "openState": "Close" + } + }, + { + "href": "/door/cooler/0", + "rep": { + "openState": "Close" + } + }, + { + "href": "/door/cvroom/0", + "rep": { + "openState": "Close" + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.countryCode": "US", + "x.com.samsung.da.region": "" + } + }, + { + "href": "/defrost/delay/0", + "rep": { + "value": false + } + }, + { + "href": "/defrost/delay/vs/0", + "rep": { + "x.com.samsung.da.delayDefrost": "Off" + } + }, + { + "href": "/defrost/reservation/vs/0", + "rep": { + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Summer Season", + "x.com.samsung.da.startTime": "0000-05-01T15:00:00", + "x.com.samsung.da.period": "04:00:00", + "x.com.samsung.da.endTime": "0000-10-31T00:00:00" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Winter Season", + "x.com.samsung.da.startTime": "0000-11-01T06:00:00", + "x.com.samsung.da.period": "04:00:00", + "x.com.samsung.da.endTime": "0000-04-30T00:00:00" + } + ] + } + }, + { + "href": "/icemaker/status/0", + "rep": { + "status": "On" + } + }, + { + "href": "/icemaker/status/vs/0", + "rep": { + "x.com.samsung.da.iceMaker": "On" + } + }, + { + "href": "/refrigeration/0", + "rep": { + "defrost": false, + "rapidFreeze": false, + "rapidCool": false + } + }, + { + "href": "/refrigeration/vs/0", + "rep": { + "x.com.samsung.da.rapidFridge": "Off", + "x.com.samsung.da.rapidFreezing": "Off" + } + }, + { + "href": "/drlc/0", + "rep": { + "DRLevel": 0, + "override": false + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.drlcLevel": "0", + "x.com.samsung.da.override": "Off", + "x.com.samsung.da.durationminutes": "0" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "Micom", + "x.com.samsung.da.newVersionAvailable": "false" + } + }, + { + "href": "/filter/waterfilter/vs/0", + "rep": { + "x.com.samsung.da.filterUsage": "0", + "x.com.samsung.da.filterUsageResolution": "1", + "x.com.samsung.da.filterStatus": "normal", + "x.com.samsung.da.filterResetType": [ + "replaceable" + ] + } + }, + { + "href": "/icemaker/one/vs/0", + "rep": { + "x.com.samsung.da.iceMaker.name": "ICE_MAKER", + "x.com.samsung.da.iceMaker.state": "On", + "x.com.samsung.da.iceMaker.type": "toggle", + "x.com.samsung.da.iceMaker.iceMakingStatus": "ICESTATUS_STOP", + "x.com.samsung.da.iceType.desired": "NORMAL" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/washer_dryer_combo_device.json b/tests/fixtures/washer_dryer_combo_device.json new file mode 100644 index 0000000..698aa12 --- /dev/null +++ b/tests/fixtures/washer_dryer_combo_device.json @@ -0,0 +1,325 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/alarms/vs/0", + "rep": {} + }, + { + "href": "/diagnosis/vs/0", + "rep": { + "x.com.samsung.da.diagnosisStart": "Ready" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPower": "-500", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.cumulativePower": "1210200", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.cumulativeDate": "1784671200", + "x.com.samsung.da.cumulativeDateUTC": "1784667600" + } + }, + { + "href": "/energy/consumption/0", + "rep": {} + }, + { + "href": "/course/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2" + ], + "x.com.samsung.da.options": [ + "DeviceType_0167", + "UpdateAllow_NotAllowed", + "Course_1C", + "AiOption_On", + "LaundryOutTime_0", + "AddWashSet_0", + "AddWashAvailable_7", + "AddWashIndicator_On", + "SeamlessControl_Enable", + "KidsLockBypass_On", + "WashingTimes_25", + "DrumCleanProposal_40", + "DrumCleanLog_2024-07-10T22:36:33|2024-09-12T21:37:17|2024-12-06T02:13:28|2025-03-04T20:40:47|2025-05-25T11:02:07|2025-08-17T19:48:10|2025-10-30T20:20:13|2026-01-08T21:44:02|2026-04-02T21:17:40|2026-06-08T19:51:07", + "DetergentOnce_1", + "DetergentLeft_0", + "DetergentBase_5", + "DetergentAlarm_Off", + "DetergentType_2", + "DetergentTotal_49", + "SoftenerOnce_0", + "SoftenerLeft_0", + "SoftenerBase_0", + "SoftenerAlarm_Off", + "SoftenerType_0", + "SoftenerTotal_0", + "SpecialFunction_4", + "AvailableDelayTime_159", + "BubbleSoak_Off", + "LaundryPlannerUserSetTime_0", + "ProgressTimeSet_420BF48204D4A204F5", + "SendToDevice_Off", + "GMT_02", + "LaundryPlannerMicomSetTime_1704", + "PreWashSetting_Off", + "IntensiveSetting_Off", + "BubbleSoakSet_00F000F000F000F0F0F0F00000F000F0F00000F000000000", + "EnergyLevelSet_05030401040104020404020503020202030301010505050405", + "MostUsed_1C8410923FA67FB01F0000000000", + "PreWashAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000", + "IntensiveAvailableSet_F0F000F0F0F000F0F0F0F00000F0F0F0F00000F000000000", + "GeoFenceAlarm", + "UsagesDB_ok", + "EnergyKW_396", + "DrumCleanLog_2024-07-10T22:36:33|2024-09-12T21:37:17|2024-12-06T02:13:28|2025-03-04T20:40:47|2025-05-25T11:02:07|2025-08-17T19:48:10|2025-10-30T20:20:13|2026-01-08T21:44:02|2026-04-02T21:17:40|2026-06-08T19:51:07", + "TimeSync_NotSupported" + ], + "x.com.samsung.da.supportedOptions": [ + "41C8410923FA67FB01F1B847E923FA67FB01F1E811E933FA53FB01F1D841E923FA67FB01F1F8102923FA57FB01F25843E923FA57FB01F26841E920FA207B01F33857E933FA67FB00024841E930FA30FB00032833E923FA37FB00020857E943FA67FB01F22841E920FA30FB00023831E930FA57FB01F2F831E933FA30FB00021831E943FA57FB0002D841E923FA30FB00030843E923FA67FB000278000913FA67FB01F2880009000A67EB01F368410923FA640B11E3880009000A000B11E3980009000A000B11E3780009000A000B0002985209204A520B000" + ] + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "Off" + } + }, + { + "href": "/power/0", + "rep": { + "value": false + } + }, + { + "href": "/cycleinterface/vs/0", + "rep": {} + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/kidslock/0", + "rep": { + "value": false + } + }, + { + "href": "/operational/state/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "x.com.samsung.da.remainingTime": "02:39:00", + "x.com.samsung.da.progressPercentage": "1", + "x.com.samsung.da.progress": "None", + "x.com.samsung.da.delayEndTime": "00:00:00", + "x.com.samsung.da.supportedProgress": [ + "None", + "Weightsensing", + "Wash", + "Rinse", + "Spin", + "Finish" + ] + } + }, + { + "href": "/operational/state/0", + "rep": { + "currentMachineState": "**REDACTED**", + "machineStates": "**REDACTED**", + "jobStates": [ + "None", + "Weightsensing", + "Wash", + "Rinse", + "Spin", + "Finish" + ], + "currentJobState": "None", + "remainingTime": "02:39:00", + "progressPercentage": "1" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "DA_WM_TP2_20_COMMON|20260041|20010102011411B402A3020700000000", + "x.com.samsung.da.description": "DA_WM_TP2_20_COMMON_WW6600R/DC92-02617A_5049", + "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": "210", + "x.com.samsung.da.diagMinVersion": "1.0", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "DA_WM_TP2_20_COMMON|20260041|20010102011411B402A3020700000000", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02674A250414(F546)", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Firmware_1_DB_20260041200911390FFFFF202617412101203104FFFF(01672026004120261741_30000000)(FileDown:0)(Type:0)", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "20091139,21012031", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "Firmware_2_DB_2025984120072205042FFFFFFFFFFFFFFFFFFFFFFFFE(016720259841FFFFFFFF_30000000)(FileDown:0)(Type:0)", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "20072205,FFFFFFFF" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+01:00" + } + }, + { + "href": "/washer/vs/0", + "rep": { + "x.com.samsung.da.waterTemperature": "40", + "x.com.samsung.da.supportedWaterTemperature": [ + "None", + "Cold", + "20", + "30", + "40", + "60", + "90" + ], + "x.com.samsung.da.spinLevel": "1400", + "x.com.samsung.da.supportedSpinLevel": [ + "RinseHold", + "NoSpin", + "400", + "800", + "1000", + "1200", + "1400" + ], + "x.com.samsung.da.dryLevel": "None", + "x.com.samsung.da.supportedDryLevel": [ + "None", + "Cupboard", + "30", + "60", + "90", + "120", + "180", + "240" + ], + "x.com.samsung.da.rinseCycles": "2", + "x.com.samsung.da.supportedRinseCycles": [ + "0", + "1", + "2", + "3", + "4", + "5" + ] + } + }, + { + "href": "/st/washercourse/vs/0", + "rep": { + "x.com.samsung.da.st.washerMode": "Table_02_Course_1C", + "x.com.samsung.da.st.courseTable": "Table_02" + } + }, + { + "href": "/setting/vs/0", + "rep": { + "x.com.samsung.da.supportedSetLanguage": [ + "ko_KR", + "en_US" + ], + "x.com.samsung.da.setLanguage": "en_GB" + } + }, + { + "href": "/wm/editcourse/vs/0", + "rep": { + "x.com.samsung.da.editCourseList": "EditCourseList_1C3026241E293633281D27211B202F2223323825392D1F37", + "x.com.samsung.da.fixedCourseList": "FixedCourseList_1C353A29" + } + }, + { + "href": "/wm/setinfo/vs/0", + "rep": { + "x.com.samsung.da.isModelSettingWithoutSC": "true", + "x.com.samsung.da.aiCourse": "true_formobile", + "x.com.samsung.da.isModelSettingPowerOnOff": "false" + } + }, + { + "href": "/wm/jobbeginingstatus/vs/0", + "rep": { + "x.com.samsung.da.currentStatus": "None" + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "Micom", + "x.com.samsung.da.newVersionAvailable": "false" + } + }, + { + "href": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "false" + } + }, + { + "href": "/remotectrl/0", + "rep": { + "value": false + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.countryCode": "GB" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/washer_flexwash_device.json b/tests/fixtures/washer_flexwash_device.json new file mode 100644 index 0000000..61aec09 --- /dev/null +++ b/tests/fixtures/washer_flexwash_device.json @@ -0,0 +1,253 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/realtimenotiforclient/vs/0", + "rep": { + "x.com.samsung.da.timeforshortnoti": "0", + "x.com.samsung.da.periodicnotisubscription": "true" + } + }, + { + "href": "/alarms/vs/0", + "rep": {} + }, + { + "href": "/diagnosis/vs/0", + "rep": { + "x.com.samsung.da.diagnosisStart": "Ready" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.instantaneousPowerUnit": "W", + "x.com.samsung.da.instantaneousPower": "-500", + "x.com.samsung.da.cumulativePower": "199200", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.cumulativeDate": "1784631600", + "x.com.samsung.da.cumulativeDateUTC": "1784646000" + } + }, + { + "href": "/energy/consumption/0", + "rep": {} + }, + { + "href": "/course/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2" + ], + "x.com.samsung.da.options": [ + "DeviceType_0146", + "UpdateAllow_NotAllowed", + "Course_A5", + "LaundryOutTime_0", + "AddGarmentIndicator_Off", + "SteamWash_Enable", + "SeamlessControl_Disable", + "KidsLockBypass_On", + "WashingTimes_14", + "DrumCleanProposal_40", + "DetergentOnce_0", + "DetergentLeft_0", + "DetergentBase_0", + "DetergentAlarm_Off", + "DetergentType_0", + "DetergentTotal_0", + "SpecialFunction_4", + "AvailableDelayTime_124", + "LaundryPlannerUserSetTime_0", + "ProgressTimeSet_4210E08209F6A2021C", + "EnergyLevelSet_0502040502030302030105", + "UsagesDB_ok", + "EnergyKW_396", + "DrumCleanLog_2023-08-11T19:17:11|2023-11-02T03:23:13|2023-12-22T22:35:54|2024-04-08T01:27:32|2024-04-22T01:07:26|2024-10-02T19:16:45|2025-02-03T02:01:33|2025-06-16T17:36:13|2026-01-04T18:34:46|2026-05-26T02:33:16", + "TimeSync_NotSupported" + ], + "x.com.samsung.da.supportedOptions": [ + "301833EA41BC20E9C843EA41FC30EA98520A41FC20EA2810EA207C20EAA831EA41FC20EA5830EA20FC20E9F831EA41FC10E9E830EA21FC20E9D8000A41FC000B18520A410C000" + ] + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "On" + } + }, + { + "href": "/power/0", + "rep": { + "value": true + } + }, + { + "href": "/cycleinterface/vs/0", + "rep": {} + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/kidslock/0", + "rep": { + "value": false + } + }, + { + "href": "/operational/state/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "x.com.samsung.da.remainingTime": "02:04:00", + "x.com.samsung.da.progressPercentage": "1", + "x.com.samsung.da.progress": "None", + "x.com.samsung.da.delayEndTime": "00:00:00", + "x.com.samsung.da.supportedProgress": [ + "None", + "Wash", + "Rinse", + "Spin", + "Finish" + ] + } + }, + { + "href": "/operational/state/0", + "rep": { + "currentMachineState": "**REDACTED**", + "machineStates": "**REDACTED**", + "jobStates": [ + "None", + "Wash", + "Rinse", + "Spin", + "Finish" + ], + "currentJobState": "None", + "remainingTime": "02:04:00", + "progressPercentage": "1" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000", + "x.com.samsung.da.description": "DA_WM_A51_20_COMMON_WV9600M/DC92-01980B_0014", + "x.com.samsung.da.serialNum": "REDACTED", + "x.com.samsung.da.otnDUID": "REDACTED", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.number": "02198A230708(E257)", + "x.com.samsung.da.newVersionAvailable": "0" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "DA_WM_A51_20_COMMON", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.number": "18030902,18090603", + "x.com.samsung.da.newVersionAvailable": "0" + } + ] + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "-04:00" + } + }, + { + "href": "/washer/vs/0", + "rep": { + "x.com.samsung.da.waterTemperature": "Warm", + "x.com.samsung.da.supportedWaterTemperature": [ + "None", + "Cold", + "Cool", + "Warm", + "Hot", + "ExtraHot" + ], + "x.com.samsung.da.spinLevel": "Low", + "x.com.samsung.da.supportedSpinLevel": [ + "RinseHold", + "NoSpin", + "Low", + "Medium", + "High" + ], + "x.com.samsung.da.soilLevel": "Heavy", + "x.com.samsung.da.supportedSoilLevel": [ + "None", + "Light", + "Normal", + "Heavy" + ], + "x.com.samsung.da.rinseCycles": "3" + } + }, + { + "href": "/st/washercourse/vs/0", + "rep": { + "x.com.samsung.da.st.washerMode": "Table_00_Course_A5", + "x.com.samsung.da.st.courseTable": "Table_00" + } + }, + { + "href": "/setting/vs/0", + "rep": {} + }, + { + "href": "/wm/editcourse/vs/0", + "rep": {} + }, + { + "href": "/wm/setinfo/vs/0", + "rep": { + "x.com.samsung.da.isModelSettingWithoutSC": "false", + "x.com.samsung.da.isModelSettingPowerOnOff": "false" + } + }, + { + "href": "/wm/jobbeginingstatus/vs/0", + "rep": {} + }, + { + "href": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "true" + } + }, + { + "href": "/remotectrl/0", + "rep": { + "value": true + } + }, + { + "href": "/configuration/vs/0", + "rep": { + "x.com.samsung.da.region": "0000000000", + "x.com.samsung.da.countryCode": "US" + } + } + ] +} \ No newline at end of file diff --git a/tests/test_by_type.py b/tests/test_by_type.py index 511aca8..9eaf708 100644 --- a/tests/test_by_type.py +++ b/tests/test_by_type.py @@ -190,6 +190,16 @@ class TestForDeviceByModel: assert reg is not None assert reg.name == 'washer' + def test_washer_wv_prefix(self): + """FlexWash twin washers use the WV consumer-model prefix -- issue #19.""" + from custom_components.localthings.registry.by_type import for_device_by_model + reg = for_device_by_model( + 'DA_WM_A51_20_COMMON|20198042|20020001001111400203000000000000', + 'DA_WM_A51_20_COMMON_WV9600M/DC92-01980B_0014', + ) + assert reg is not None + assert reg.name == 'washer' + def test_unknown_model_returns_none(self): from custom_components.localthings.registry.by_type import for_device_by_model reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD') diff --git a/tests/test_fridge_capabilities.py b/tests/test_fridge_capabilities.py index 068956d..cc4daea 100644 --- a/tests/test_fridge_capabilities.py +++ b/tests/test_fridge_capabilities.py @@ -104,6 +104,43 @@ class TestRefrigerationFallback: assert desc.exists_fn({}, {'/refrigeration/0': {}}) is True +class TestFlexZone: + """Issue #32 (surfaced by #26/#27's TP2X/Bespoke dumps): the flex-zone + entry's prefix varies by fridge family (CV_TTYPE_RF9000A_ on RF9000-class, + CV_FDR_ on TP1X/Bespoke-class). The old hardcoded-prefix match left + CV_FDR_-family fridges with an always-unknown select, and a write would + have appended a duplicate flag instead of replacing the existing one.""" + + def test_reads_rf9000_prefix(self): + rep = { + 'x.com.samsung.da.modes': ['CV_TTYPE_RF9000A_BEVERAGE', 'WATERFILTER_DISABLE'], + 'x.com.samsung.da.supportedOptions': [ + 'CV_TTYPE_RF9000A_FREEZE', 'CV_TTYPE_RF9000A_BEVERAGE'], + } + assert fridge._flex_zone_current(rep) == 'CV_TTYPE_RF9000A_BEVERAGE' + + def test_reads_cv_fdr_prefix(self): + rep = { + 'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'CV_FDR_MEAT', 'WATERFILTER_ENABLE'], + 'x.com.samsung.da.supportedOptions': [ + 'CV_FDR_WINE', 'CV_FDR_DELI', 'CV_FDR_BEVERAGE', 'CV_FDR_MEAT'], + } + assert fridge._flex_zone_current(rep) == 'CV_FDR_MEAT' + + def test_write_replaces_rather_than_duplicates(self): + rep = { + 'x.com.samsung.da.modes': ['CVN_CONVERTIBLE_ZONE', 'CV_FDR_MEAT', 'WATERFILTER_ENABLE'], + 'x.com.samsung.da.supportedOptions': [ + 'CV_FDR_WINE', 'CV_FDR_DELI', 'CV_FDR_BEVERAGE', 'CV_FDR_MEAT'], + } + path, payload = fridge._flex_zone_write('CV_FDR_WINE', rep) + assert path == ['mode', 'vs', '0'] + modes = payload['x.com.samsung.da.modes'] + assert modes.count('CV_FDR_WINE') == 1 + assert 'CV_FDR_MEAT' not in modes + assert 'CVN_CONVERTIBLE_ZONE' in modes and 'WATERFILTER_ENABLE' in modes + + class TestTp1xNativeDuplicateResources: """The US TP1X_REF_21K publishes two native mirrors in addition to the richer vendor resources. They must count as covered without producing @@ -134,3 +171,60 @@ class TestTp1xNativeDuplicateResources: assert 'ice_maker_enabled' not in state assert state['icemaker_one_enabled'] is True assert state['icemaker_two_enabled'] is True + + +class TestPantryZone: + """Cool Select Zone pantry compartment on ARTIK051_REF_17K -- issue #20.""" + + def test_href(self): + assert fridge.PANTRY_ZONE.href == '/status/pantry/one/vs/0' + + def test_write(self): + desc = fridge.PANTRY_ZONE.entities[0] + path, body = desc.write_fn('FDR_WINE', {}) + assert path == ['status', 'pantry', 'one', 'vs', '0'] + assert body == {'x.com.samsung.da.mode': 'FDR_WINE'} + + +class TestArtik051AndTp2xFixturesHaveCompleteCoverage: + """issue #20 (ARTIK051_REF_17K) and #26 (TP2X_REF_20K) both triggered + the incomplete-capability-coverage repair; both must resolve to zero + unbound hrefs now that /diagnosis/vs/0 (dishwasher.DIAGNOSIS, reused for + fridges), /status/pantry/one/vs/0 (PANTRY_ZONE), and the OCF-native + defrost/icemaker mirrors are covered.""" + + def test_artik051_ref_17k(self): + from custom_components.localthings.registry.adapter import flatten + from custom_components.localthings.registry.by_type import refrigerator + from custom_components.localthings.registry.discovery import discover + from tests.conftest import _load_device + + resources = _load_device('refrigerator_artik051_ref_17k') + unbound = [] + bound = discover( + resources, + refrigerator.REGISTRY.capabilities, + refrigerator.REGISTRY.pattern_capabilities, + log=unbound.append, + ) + assert unbound == [] + state = flatten(bound, resources) + assert state['pantry_zone_mode'] == 'FDR_DRINKS' + + def test_tp2x_ref_20k(self): + from custom_components.localthings.registry.adapter import flatten + from custom_components.localthings.registry.by_type import refrigerator + from custom_components.localthings.registry.discovery import discover + from tests.conftest import _load_device + + resources = _load_device('refrigerator_tp2x_ref_20k') + unbound = [] + bound = discover( + resources, + refrigerator.REGISTRY.capabilities, + refrigerator.REGISTRY.pattern_capabilities, + log=unbound.append, + ) + assert unbound == [] + state = flatten(bound, resources) + assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE' diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index ee05af3..265f226 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -95,6 +95,64 @@ def test_registry_reproduces_golden_state_keys_for_tp1x_ref_21k_us(): ) +def test_registry_reproduces_golden_state_keys_for_washer_flexwash(): + """FlexWash twin washers (WV-prefix consumer model, e.g. WV55M9600AW) + report no oneUiVersion and previously fell through for_device_by_model's + consumer-prefix map entirely -- issue #19.""" + from tests.conftest import _load_device + resources = _load_device('washer_flexwash') + golden = json.loads((GOLDEN / 'washer_flexwash.json').read_text()) + state_keys = _new_state_keys('washer_flexwash', 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_washer_dryer_combo(): + """Washer/dryer combo units carry a writable dryLevel field on + /washer/vs/0 itself, with no separate dryer resource -- issue #22.""" + from tests.conftest import _load_device + resources = _load_device('washer_dryer_combo') + golden = json.loads((GOLDEN / 'washer_dryer_combo.json').read_text()) + state_keys = _new_state_keys('washer_dryer_combo', 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_artik051_ref_17k(): + """ARTIK051_REF_17K's Cool Select Zone pantry compartment + (/status/pantry/one/vs/0) -- issue #20.""" + from tests.conftest import _load_device + resources = _load_device('refrigerator_artik051_ref_17k') + golden = json.loads((GOLDEN / 'refrigerator_artik051_ref_17k.json').read_text()) + state_keys = _new_state_keys('refrigerator_artik051_ref_17k', 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_tp2x_ref_20k(): + """TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra + energy fields (cumulativeConsumption/monthlyConsumption/ + thismonthlyConsumption) surfaced by issue #26.""" + from tests.conftest import _load_device + resources = _load_device('refrigerator_tp2x_ref_20k') + golden = json.loads((GOLDEN / 'refrigerator_tp2x_ref_20k.json').read_text()) + state_keys = _new_state_keys('refrigerator_tp2x_ref_20k', 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_resources_from_batch_preferred_over_flat(): from tests.conftest import _resources_from_dump dump = { diff --git a/tests/test_washer_capabilities.py b/tests/test_washer_capabilities.py index 1213789..ac143d8 100644 --- a/tests/test_washer_capabilities.py +++ b/tests/test_washer_capabilities.py @@ -40,6 +40,22 @@ class TestWasherSettings: assert body == {'x.com.samsung.da.rinseCycles': '3'} +class TestDryLevel: + """Washer/dryer combo units carry a writable dryLevel field on + /washer/vs/0 itself, self-gated off on plain washers -- issue #22.""" + + def test_exists_only_when_supported_dry_level_present(self): + desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'dry_level') + assert desc.exists_fn({'x.com.samsung.da.supportedDryLevel': ['None', '30']}, {}) is True + assert desc.exists_fn({}, {}) is False + + def test_write(self): + desc = next(e for e in washer.WASHER_SETTINGS.entities if e.key == 'dry_level') + path, body = desc.write_fn('Cupboard', {}) + assert path == ['washer', 'vs', '0'] + assert body == {'x.com.samsung.da.dryLevel': 'Cupboard'} + + class TestWasherCourse: def test_href(self): assert washer.WASHER_COURSE.href == '/course/vs/0' @@ -233,3 +249,46 @@ class TestDetergentSoftenerDosing: assert self._desc('detergent_low').exists_fn({'x.com.samsung.da.options': []}, {}) is False rep = {'x.com.samsung.da.options': _DOSING_OPTIONS} assert self._desc('detergent_low').exists_fn(rep, {}) is True + + +class TestFlexWashAndComboFixturesHaveCompleteCoverage: + """FlexWash (issue #19, previously unrecognized entirely) and + washer/dryer combo (issue #22, dry_level) dumps must both resolve to + zero unbound hrefs.""" + + def test_flexwash(self): + from custom_components.localthings.registry.adapter import flatten + from custom_components.localthings.registry.by_type import washer as washer_registry + from custom_components.localthings.registry.discovery import discover + from tests.conftest import _load_device + + resources = _load_device('washer_flexwash') + unbound = [] + bound = discover( + resources, + washer_registry.REGISTRY.capabilities, + washer_registry.REGISTRY.pattern_capabilities, + log=unbound.append, + ) + assert unbound == [] + state = flatten(bound, resources) + assert 'dry_level' not in state # plain washer -- no supportedDryLevel field + + def test_dryer_combo(self): + from custom_components.localthings.registry.adapter import flatten + from custom_components.localthings.registry.by_type import washer as washer_registry + from custom_components.localthings.registry.discovery import discover + from tests.conftest import _load_device + + resources = _load_device('washer_dryer_combo') + unbound = [] + bound = discover( + resources, + washer_registry.REGISTRY.capabilities, + washer_registry.REGISTRY.pattern_capabilities, + log=unbound.append, + ) + assert unbound == [] + state = flatten(bound, resources) + # the device's own "off" sentinel is the literal string 'None', not absence + assert state['dry_level'] == 'None'