diff --git a/custom_components/localthings/registry/by_type/refrigerator.py b/custom_components/localthings/registry/by_type/refrigerator.py index 4c0abf5..b834820 100644 --- a/custom_components/localthings/registry/by_type/refrigerator.py +++ b/custom_components/localthings/registry/by_type/refrigerator.py @@ -25,6 +25,7 @@ REGISTRY = DeviceRegistry( fridge.DEFROST_DELAY, fridge.DEFROST_DELAY_NATIVE_DUPLICATE, fridge.DEFROST_BLOCK_STATUS, + fridge.DEFINITE_TEMPERATURE_COOLER, 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 14e7436..e7e08a1 100644 --- a/custom_components/localthings/registry/capabilities/fridge.py +++ b/custom_components/localthings/registry/capabilities/fridge.py @@ -108,6 +108,37 @@ TEMP_SETPOINT = Capability( ), ) +# --------------------------------------------------------------------------- +# Discrete cooler setpoint (issue #186) -- some single-door ("cooler only") +# fridges report no /temperature/current|desired/* pair at all (this href +# doesn't match TEMP_CURRENT_GENERIC/TEMP_SETPOINT's '/temperature/current/' +# or '/temperature/desired/' prefixes), only this one vendor resource that +# bundles the live desired value together with the *specific* values the +# unit accepts. That supportedList (e.g. ['1','2','3','4','7'] on the issue +# #186 dump) is not a contiguous range -- 5 and 6 genuinely aren't valid +# setpoints on this model -- so a NumberDesc with a min/max/step would let a +# user pick an unsupported value; modeled as a select reading its own live +# options list instead, same shape as BEVERAGE_ZONE/PANTRY_ZONE above. +# --------------------------------------------------------------------------- + +def _definite_cooler_write(p, rep, href=None): + return (['temperature', 'definite', 'cooler', 'vs', '0'], + {'x.com.samsung.da.definite.desired': p}) + + +DEFINITE_TEMPERATURE_COOLER = Capability( + href='/temperature/definite/cooler/vs/0', + poll_tier='warm', + entities=( + SelectDesc(key='cooler_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_cooler_write), + ), +) + # --------------------------------------------------------------------------- # Icemaker nighttime quiet mode # --------------------------------------------------------------------------- diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 7822ab1..c904275 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -214,6 +214,16 @@ "100": "High" } }, + "cooler_temperature_setpoint": { + "name": "Cooler temperature", + "state": { + "1": "1 °C", + "2": "2 °C", + "3": "3 °C", + "4": "4 °C", + "7": "7 °C" + } + }, "day_brightness": { "name": "Cabinet brightness", "state": { diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 833e3cc..e864461 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -214,6 +214,16 @@ "100": "Hoog" } }, + "cooler_temperature_setpoint": { + "name": "Koeler temperatuur", + "state": { + "1": "1 °C", + "2": "2 °C", + "3": "3 °C", + "4": "4 °C", + "7": "7 °C" + } + }, "day_brightness": { "name": "Helderheid kast", "state": { diff --git a/tests/fixtures/golden/refrigerator_definite_cooler.json b/tests/fixtures/golden/refrigerator_definite_cooler.json new file mode 100644 index 0000000..24d5970 --- /dev/null +++ b/tests/fixtures/golden/refrigerator_definite_cooler.json @@ -0,0 +1,27 @@ +{ + "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", + "energy_kwh", + "energy_saved_kwh", + "firmware_update", + "fridge_sound", + "night_end", + "night_start", + "power_energy_kwh", + "power_watts", + "rapid_freezing", + "rapid_fridge", + "selfcheck_error", + "selfcheck_result", + "selfcheck_status" + ] +} \ No newline at end of file diff --git a/tests/fixtures/refrigerator_definite_cooler_device.json b/tests/fixtures/refrigerator_definite_cooler_device.json new file mode 100644 index 0000000..384d933 --- /dev/null +++ b/tests/fixtures/refrigerator_definite_cooler_device.json @@ -0,0 +1,401 @@ +{ + "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": "On", + "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": "3", + "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": "Off", + "level.brightness.daytime": "100", + "level.brightness.nighttime": "100", + "night.starttime": "N/A", + "night.duration.minute": "N/A", + "timezone.offset": "-03:00", + "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/cooler/vs/0", + "rep": { + "x.com.samsung.da.definite.desired": "2", + "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/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": "9", + "x.com.samsung.da.instantaneousPower": "51", + "x.com.samsung.da.cumulativePower": "593883", + "x.com.samsung.da.cumulativeSavedPower": "0", + "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": "-03:00", + "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" + ], + "href": "/energy/ailevel/vs/0" + } + }, + { + "href": "/information/vs/0", + "rep": { + "x.com.samsung.da.modelNum": "TP1X_REF_21K|00174941|0006012A001811304100000020010000", + "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": "24010202, 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.childlock": "Locked", + "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.supportedModes": [ + "HOMECARE_WIZARD_V2", + "ENERGY_REPORT_MODEL", + "18K_REF_OUTDOOR_CONTROL_V2" + ], + "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": "/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": "035000174941FFFFFFFF", + "versions": [ + "24010202", + "FFFFFFFF" + ], + "visVersion": "240102" + } + ] + } + }, + { + "href": "/timezone/vs/0", + "rep": { + "timezoneid": "America/Sao_Paulo", + "offset": "-03: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": "Pumba_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 + } + } + } + ] +} \ No newline at end of file diff --git a/tests/test_fridge_capabilities.py b/tests/test_fridge_capabilities.py index 3b6399e..d8d6d80 100644 --- a/tests/test_fridge_capabilities.py +++ b/tests/test_fridge_capabilities.py @@ -230,6 +230,44 @@ class TestPantryZone: assert body == {'x.com.samsung.da.mode': 'FDR_WINE'} +class TestDefiniteTemperatureCooler: + """Discrete cooler setpoint (RT42DG6630B1FZ, issue #186) -- a single-door + fridge whose /temperature/definite/cooler/vs/0 falls outside both + TEMP_CURRENT_GENERIC and TEMP_SETPOINT's href prefixes, so temperature + control was entirely unbound before this capability was added.""" + + def test_href(self): + assert fridge.DEFINITE_TEMPERATURE_COOLER.href == '/temperature/definite/cooler/vs/0' + + def test_write(self): + desc = fridge.DEFINITE_TEMPERATURE_COOLER.entities[0] + path, body = desc.write_fn('3', {}) + assert path == ['temperature', 'definite', 'cooler', 'vs', '0'] + assert body == {'x.com.samsung.da.definite.desired': '3'} + + 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_definite_cooler') + 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'] == '2' + + rep = resources['/temperature/definite/cooler/vs/0'] + assert rep['x.com.samsung.da.definite.supportedList'] == ['1', '2', '3', '4', '7'] + + 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 8502a91..29303ea 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -895,6 +895,28 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_artik051_krac_ ) +def test_registry_reproduces_golden_state_keys_for_refrigerator_definite_cooler(): + """RT42DG6630B1FZ (issue #186) -- a single-door "cooler only" fridge whose + /temperature/definite/cooler/vs/0 doesn't match either + TEMP_CURRENT_GENERIC's '/temperature/current/' or TEMP_SETPOINT's + '/temperature/desired/' href prefix, so temperature control was entirely + unbound. Resolved via the new DEFINITE_TEMPERATURE_COOLER capability + (a select over the device's own discrete supportedList, not a + continuous NumberDesc range -- 5 and 6 aren't valid setpoints on this + model). Binds cleanly with zero unbound hrefs.""" + from tests.conftest import _load_device + resources = _load_device('refrigerator_definite_cooler') + golden = json.loads( + (GOLDEN / 'refrigerator_definite_cooler.json').read_text() + ) + state_keys = _new_state_keys('refrigerator_definite_cooler', 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 = {