diff --git a/custom_components/localthings/registry/by_type/refrigerator.py b/custom_components/localthings/registry/by_type/refrigerator.py index 86228b7..8e4f684 100644 --- a/custom_components/localthings/registry/by_type/refrigerator.py +++ b/custom_components/localthings/registry/by_type/refrigerator.py @@ -28,6 +28,7 @@ REGISTRY = DeviceRegistry( fridge.DEFROST_DELAY_NATIVE_DUPLICATE, fridge.DEFROST_BLOCK_STATUS, fridge.DEFINITE_TEMPERATURE_COOLER, + fridge.DEFINITE_TEMPERATURE_FREEZER, fridge.DOORS_FALLBACK, fridge.TEMPERATURES_FALLBACK, fridge.ICEMAKER_STATUS_FALLBACK, diff --git a/custom_components/localthings/registry/capabilities/fridge.py b/custom_components/localthings/registry/capabilities/fridge.py index 512f0fe..42a827d 100644 --- a/custom_components/localthings/registry/capabilities/fridge.py +++ b/custom_components/localthings/registry/capabilities/fridge.py @@ -163,6 +163,34 @@ DEFINITE_TEMPERATURE_COOLER = Capability( ), ) + +def _definite_freezer_write(p, rep, href=None): + return ( + ["temperature", "definite", "freezer", "vs", "0"], + {"x.com.samsung.da.definite.desired": p}, + ) + + +# Freezer half of the same discrete-setpoint pattern (issue #229): a +# fridge/freezer combo reporting no /temperature/current|desired/freezer +# pair, only this bundled vendor resource -- identical shape to +# DEFINITE_TEMPERATURE_COOLER above (down to the field names), just negative +# supportedList values (e.g. ['-23','-21','-19','-17','-15']). +DEFINITE_TEMPERATURE_FREEZER = Capability( + href="/temperature/definite/freezer/vs/0", + poll_tier="warm", + entities=( + SelectDesc( + key="freezer_temperature_setpoint", + field="x.com.samsung.da.definite.desired", + icon="mdi:thermometer", + entity_category="config", + options_field="x.com.samsung.da.definite.supportedList", + write_fn=_definite_freezer_write, + ), + ), +) + # --------------------------------------------------------------------------- # Icemaker nighttime quiet mode # --------------------------------------------------------------------------- diff --git a/custom_components/localthings/translations/cs.json b/custom_components/localthings/translations/cs.json index ca69505..bff44e0 100644 --- a/custom_components/localthings/translations/cs.json +++ b/custom_components/localthings/translations/cs.json @@ -643,6 +643,9 @@ }, "air_filter_pm1_threshold": { "name": "Práh alarmu PM1 filtru" + }, + "freezer_temperature_setpoint": { + "name": "Teplota mrazicí zóny" } }, "sensor": { diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 0a620e2..2ca13af 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -643,6 +643,9 @@ }, "air_filter_pm1_threshold": { "name": "PM1 filter alarm threshold" + }, + "freezer_temperature_setpoint": { + "name": "Freezer temperature" } }, "sensor": { diff --git a/custom_components/localthings/translations/es.json b/custom_components/localthings/translations/es.json index cf5782c..902b1d6 100644 --- a/custom_components/localthings/translations/es.json +++ b/custom_components/localthings/translations/es.json @@ -757,6 +757,9 @@ }, "air_filter_pm1_threshold": { "name": "Umbral de alarma del filtro PM1" + }, + "freezer_temperature_setpoint": { + "name": "Temperatura del congelador" } }, "sensor": { diff --git a/custom_components/localthings/translations/it.json b/custom_components/localthings/translations/it.json index 6fdda18..c0a5beb 100644 --- a/custom_components/localthings/translations/it.json +++ b/custom_components/localthings/translations/it.json @@ -643,6 +643,9 @@ }, "air_filter_pm1_threshold": { "name": "Soglia di allarme del filtro PM1" + }, + "freezer_temperature_setpoint": { + "name": "Temperatura freezer" } }, "sensor": { diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index d2ed9a7..955bfc9 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -643,6 +643,9 @@ }, "air_filter_pm1_threshold": { "name": "PM1-filteralarmdrempel" + }, + "freezer_temperature_setpoint": { + "name": "Temperatuur vriesgedeelte" } }, "sensor": { diff --git a/tests/fixtures/golden/refrigerator_tp1x_ref_21k_definite.json b/tests/fixtures/golden/refrigerator_tp1x_ref_21k_definite.json new file mode 100644 index 0000000..84b6148 --- /dev/null +++ b/tests/fixtures/golden/refrigerator_tp1x_ref_21k_definite.json @@ -0,0 +1,30 @@ +{ + "state_keys": [ + "ai_energy_level", + "alarm_code", + "auto_door_opener", + "brightness_level", + "cabinet_light_dim", + "cabinet_light_switch", + "cooler_temperature_setpoint", + "day_brightness", + "door_alert", + "door_cooler_open", + "door_freezer_open", + "energy_kwh", + "energy_saved_kwh", + "firmware_update", + "flex_zone_mode", + "freezer_temperature_setpoint", + "fridge_sound", + "night_end", + "night_start", + "power_energy_kwh", + "power_watts", + "rapid_freezing", + "rapid_fridge", + "selfcheck_error", + "selfcheck_result", + "selfcheck_status" + ] +} diff --git a/tests/fixtures/refrigerator_tp1x_ref_21k_definite_device.json b/tests/fixtures/refrigerator_tp1x_ref_21k_definite_device.json new file mode 100644 index 0000000..54c2cd3 --- /dev/null +++ b/tests/fixtures/refrigerator_tp1x_ref_21k_definite_device.json @@ -0,0 +1,467 @@ +{ + "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": "/cabinet/light/total/vs/0", + "rep": { + "x.com.samsung.da.lightLevel": "100", + "x.com.samsung.da.lightResolution": "3", + "x.com.samsung.da.lightControl.off.include": "Off", + "x.com.samsung.da.lightControl": "Off", + "x.com.samsung.da.lightControl.hide": "true", + "light.dimming.status": "Off", + "href": "/cabinet/light/total/vs/0", + "rt": [ + "x.com.samsung.da.cabinetlight" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/settings/sound/alert/door/vs/0", + "rep": { + "alert.door": "1", + "supportedAlert.door": [ + "1", + "2", + "3", + "4" + ], + "href": "/settings/sound/alert/door/vs/0", + "rt": [ + "x.com.samsung.alert.door" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/cabinet/light/enhanced/vs/0", + "rep": { + "light.control.status": "On", + "level.brightness.daytime": "100", + "level.brightness.nighttime": "66", + "night.starttime": "2026-07-31T15:30:00", + "night.duration.minute": "540", + "timezone.offset": "+05:30", + "href": "/cabinet/light/enhanced/vs/0", + "rt": [ + "x.com.samsung.da.light.enhanced" + ], + "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": "/temperature/definite/freezer/vs/0", + "rep": { + "x.com.samsung.da.definite.desired": "-19", + "x.com.samsung.da.definite.supportedList": [ + "-23", + "-21", + "-19", + "-17", + "-15" + ], + "x.com.samsung.da.unit": "Celsius", + "href": "/temperature/definite/freezer/vs/0" + } + }, + { + "href": "/temperature/definite/cooler/vs/0", + "rep": { + "x.com.samsung.da.definite.desired": "4", + "x.com.samsung.da.definite.supportedList": [ + "1", + "2", + "3", + "4", + "7" + ], + "x.com.samsung.da.unit": "Celsius", + "href": "/temperature/definite/cooler/vs/0" + } + }, + { + "href": "/door/freezer/0", + "rep": { + "openState": "Close", + "href": "/door/freezer/0", + "rt": [ + "oic.r.door" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/door/cooler/0", + "rep": { + "openState": "Close", + "href": "/door/cooler/0", + "rt": [ + "oic.r.door" + ], + "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": "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" + } + ], + "href": "/doors/vs/0" + } + }, + { + "href": "/drlc/vs/0", + "rep": { + "x.com.samsung.da.drlcLevel": "0", + "x.com.samsung.da.override": "Not_Supported", + "x.com.samsung.da.durationminutes": "0", + "x.com.samsung.da.realSaving": "Off", + "href": "/drlc/vs/0" + } + }, + { + "href": "/energy/consumption/vs/0", + "rep": { + "x.com.samsung.da.cumulativeConsumption": "1274", + "x.com.samsung.da.instantaneousPower": "46", + "x.com.samsung.da.cumulativePower": "34106", + "x.com.samsung.da.cumulativeSavedPower": "0", + "x.com.samsung.da.cumulativeUnit": "Wh", + "x.com.samsung.da.instantaneousPowerUnit": "W", + "href": "/energy/consumption/vs/0", + "x.com.samsung.da.cumulativeDateUTC": "1785402720" + } + }, + { + "href": "/file/information/vs/0", + "rep": { + "x.com.samsung.timeoffset": "+05:30", + "x.com.samsung.supprtedtype": 1, + "href": "/file/information/vs/0" + } + }, + { + "href": "/refrigeration/vs/0", + "rep": { + "x.com.samsung.da.rapidFridge": "Off", + "x.com.samsung.da.rapidFreezing": "Off", + "href": "/refrigeration/vs/0", + "rt": [ + "x.com.samsung.da.fridge" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/energy/ailevel/vs/0", + "rep": { + "aiLevel": "1", + "supportedAiLevel": [ + "1", + "2" + ], + "href": "/energy/ailevel/vs/0" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP1X_REF_21K|70657341|00060123001811344100000020090400", + "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": "RT1", + "x.com.samsung.da.diagProtocolType": "BLE_OCF", + "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": "260617", + "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": "2602020C, FFFFFFFF, 24062000, FFFFFFFF", + "x.com.samsung.da.newVersionAvailable": "0" + } + ], + "href": "/information/vs/0" + } + }, + { + "href": "/status/lock/vs/0", + "rep": { + "x.com.samsung.da.device.sound": "On", + "x.com.samsung.da.doorAlarmSound": "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": [ + "RVACATION_OFF", + "FCONVERT_FREEZER" + ], + "x.com.samsung.da.supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2", + "VACATION_MODE_CONTROL" + ], + "x.com.samsung.da.supportedOptions": [ + "FCONVERT_FREEZER", + "FCONVERT_FRIDGE", + "FCONVERT_OFF" + ], + "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": "/rm/control/vs/0", + "rep": { + "minPeriod": "9000", + "href": "/rm/control/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-COM", + "versions": [ + "20260617" + ], + "visVersion": "260617" + }, + { + "type": "Micom", + "modelId": "835070657341FFFFFFFF", + "versions": [ + "2602020C", + "FFFFFFFF" + ], + "visVersion": "260202" + }, + { + "type": "Micom", + "modelId": "035070657341FFFFFFFF", + "versions": [ + "2602020C", + "FFFFFFFF" + ], + "visVersion": "260202" + }, + { + "type": "Micom", + "modelId": "035070658641FFFFFFFF", + "versions": [ + "24062000", + "FFFFFFFF" + ], + "visVersion": "240620" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "Asia/Kolkata", + "offset": "+05:30", + "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": "IoT0" + } + }, + { + "href": "/quickcontrol/info/vs/0", + "rep": { + "supportedVersion": "1.0" + } + }, + { + "href": "/dginformation/vs/0", + "rep": { + "enrolmentstatus": "UnEnrolled", + "devicestate": "Deactivated", + "lockstatus": "Unknown", + "nextduedate": "", + "workingminutes": 0, + "paymentinfo": { + "emiplan": "Unknown", + "currency": "Unknown", + "totalemi": 0, + "totalemipaid": 0 + } + } + } + ] +} diff --git a/tests/test_fridge_capabilities.py b/tests/test_fridge_capabilities.py index 3d755ae..6ad4e4b 100644 --- a/tests/test_fridge_capabilities.py +++ b/tests/test_fridge_capabilities.py @@ -329,6 +329,56 @@ class TestDefiniteTemperatureCooler: assert rep["x.com.samsung.da.definite.supportedList"] == ["1", "2", "3", "4", "7"] +class TestDefiniteTemperatureFreezer: + """Discrete freezer setpoint (TP1X_REF_21K, issue #229) -- the same + definite-setpoint pattern as DEFINITE_TEMPERATURE_COOLER above, on a + fridge/freezer combo that reports it for *both* compartments. Identical + field shape, just negative supportedList values.""" + + def test_href(self): + assert fridge.DEFINITE_TEMPERATURE_FREEZER.href == "/temperature/definite/freezer/vs/0" + + def test_write(self): + desc = next( + e for e in fridge.DEFINITE_TEMPERATURE_FREEZER.entities if isinstance(e, SelectDesc) + ) + assert desc.write_fn is not None + result = desc.write_fn("-19", {}) + assert result is not None + path, body = result + assert path == ["temperature", "definite", "freezer", "vs", "0"] + assert body == {"x.com.samsung.da.definite.desired": "-19"} + + def test_no_unbound_hrefs_and_discrete_options(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_tp1x_ref_21k_definite") + unbound = [] + bound = discover( + resources, + refrigerator.REGISTRY.capabilities, + refrigerator.REGISTRY.pattern_capabilities, + log=unbound.append, + ) + assert unbound == [] + + state = flatten(bound, resources) + assert state["cooler_temperature_setpoint"] == "4" + assert state["freezer_temperature_setpoint"] == "-19" + + rep = resources["/temperature/definite/freezer/vs/0"] + assert rep["x.com.samsung.da.definite.supportedList"] == [ + "-23", + "-21", + "-19", + "-17", + "-15", + ] + + class TestKimchiZone: """Kimchi-refrigerator compartments (TP2X_REF_20K-class, issue #26) -- top/middle/bottom each report an identically-shaped currentMode/ diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 8cfa8ac..dafc1fb 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -1296,6 +1296,23 @@ def test_registry_reproduces_golden_state_keys_for_dryer_tp1_21_drum_clean(): ) +def test_registry_reproduces_golden_state_keys_for_refrigerator_tp1x_ref_21k_definite(): + """TP1X_REF_21K (issue #229) reports the discrete definite-setpoint + pattern (issue #186) on *both* compartments -- previously only the + cooler half was modeled; DEFINITE_TEMPERATURE_FREEZER covers the + freezer's identical-shape /temperature/definite/freezer/vs/0.""" + from tests.conftest import _load_device + + resources = _load_device("refrigerator_tp1x_ref_21k_definite") + golden = json.loads((GOLDEN / "refrigerator_tp1x_ref_21k_definite.json").read_text()) + state_keys = _new_state_keys("refrigerator_tp1x_ref_21k_definite", 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