Support 2-axis wind oscillation and overload-protection status on newer WindFree boards (issue #126)
Newer TP1X_DA-AC-RAC-01011_0000 firmware (Bespoke AI WindFree Deluxe, AR60H10D1JWNME) drops /wind/direction/vs/0 entirely and reports swing via a separate vertical/horizontal Swing|Fix pair on /wind/oscillation/vs/0 instead, which left swing_mode/swing_modes silently empty and the href unbound. climate.py now falls back to the oscillation resource when /wind/direction/vs/0 is absent, mapping the same off/vertical/horizontal/both vocabulary the existing swing control already uses. Also binds the board's /anomalyload/vs/0 overload-response resource as read-only diagnostic sensors (operation state + mode) -- the same "don't guess" precedent as the existing CURRENT_LIMIT capability, since nothing in the dump confirms the exact behavioral difference between its 'Alarm' and 'PowerSaving' modes or whether toggling it is safe on live HVAC hardware. Most of the other gaps this issue reported (Fan-only mode, WindFree preset labels, target-temperature channel selection, power on/off via the vendor resource) turned out to already be fixed by the just-merged cool-only global RAC work.
This commit is contained in:
9 files changed
+770
-4
No files matched your search
@@ -48,6 +48,7 @@ from .registry.capabilities.airconditioner import (
|
||||
HREF_TEMPS_VS as TEMPS_VS_HREF,
|
||||
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
|
||||
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
||||
HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF,
|
||||
HREF_CONVENIENT as CONVENIENT_HREF,
|
||||
)
|
||||
from .registry.capabilities.common import normalize_temp_unit
|
||||
@@ -120,6 +121,26 @@ _DEVICE_TO_SWING: dict[str, str] = {
|
||||
}
|
||||
_SWING_TO_DEVICE = {v: k for k, v in _DEVICE_TO_SWING.items()}
|
||||
|
||||
# Swing fallback via /wind/oscillation/vs/0 (issue #126) -- boards without
|
||||
# WIND_DIRECTION_HREF at all report two independent Swing|Fix toggles
|
||||
# instead of one combined code. Same HA vocabulary as _DEVICE_TO_SWING
|
||||
# above (off/vertical/horizontal/both), just read from/written to a pair
|
||||
# of fields rather than a single one.
|
||||
def _oscillation_swing(rep: dict) -> str | None:
|
||||
vertical = rep.get('vertical')
|
||||
horizontal = rep.get('horizontal')
|
||||
if vertical is None and horizontal is None:
|
||||
return None
|
||||
v = vertical == 'Swing'
|
||||
h = horizontal == 'Swing'
|
||||
if v and h:
|
||||
return 'both'
|
||||
if v:
|
||||
return 'vertical'
|
||||
if h:
|
||||
return 'horizontal'
|
||||
return 'off'
|
||||
|
||||
# Preset (convenient mode): resolved dynamically from the device's own
|
||||
# /mode/convenient/vs/0 supportedModes -- no per-model table. The device 'Off'
|
||||
# code maps to HA's PRESET_NONE ("no preset active"); every other code is
|
||||
@@ -358,13 +379,25 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
def fan_modes(self) -> list[str]:
|
||||
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
|
||||
def _swing_via_direction(self) -> bool:
|
||||
"""True when WIND_DIRECTION_HREF is the swing channel to use --
|
||||
signalled by its presence. Boards without it (issue #126) report
|
||||
the 2-axis oscillation resource instead; see _oscillation_swing."""
|
||||
return bool(self._rep(WIND_DIRECTION_HREF))
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
if self._swing_via_direction():
|
||||
return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
return _oscillation_swing(self._rep(WIND_OSCILLATION_HREF))
|
||||
|
||||
@property
|
||||
def swing_modes(self) -> list[str]:
|
||||
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
if self._swing_via_direction():
|
||||
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
if self._rep(WIND_OSCILLATION_HREF):
|
||||
return list(_SWING_TO_DEVICE.keys())
|
||||
return []
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
@@ -428,7 +461,12 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||
if self._swing_via_direction():
|
||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||
return
|
||||
if self._rep(WIND_OSCILLATION_HREF):
|
||||
await self.coordinator.async_send_command(
|
||||
self._bound, ('oscillation', swing_mode))
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
if preset_mode == PRESET_AI_COMFORT:
|
||||
|
||||
@@ -25,6 +25,7 @@ REGISTRY = DeviceRegistry(
|
||||
airconditioner.DISPLAY_LIGHT,
|
||||
airconditioner.MUTE_ONCE,
|
||||
airconditioner.CURRENT_LIMIT,
|
||||
airconditioner.ANOMALY_LOAD,
|
||||
airconditioner.CURRENT_TEMPERATURE,
|
||||
airconditioner.CURRENT_TEMPERATURE_VS,
|
||||
airconditioner.HUMIDITY,
|
||||
|
||||
@@ -35,13 +35,19 @@ HREF_TEMP_DESIRED = '/temperature/desired/0' # target_temperature (write ta
|
||||
HREF_TEMP_CONTROL = '/temperature/control/vs/0' # target_temperature_step
|
||||
HREF_WIND_STRENGTH = '/wind/strength/vs/0' # fan_mode
|
||||
HREF_WIND_DIRECTION = '/wind/direction/vs/0' # swing_mode
|
||||
# Newer boards (issue #126, TP1X_DA-AC-RAC-01011 WindFree variant) report no
|
||||
# HREF_WIND_DIRECTION at all and instead carry a 2-axis oscillation resource
|
||||
# (separate vertical/horizontal Swing|Fix toggles, each with its own angle).
|
||||
# climate.py falls back to this when HREF_WIND_DIRECTION is absent, the same
|
||||
# duality pattern as the OCF/vendor temperature channel below.
|
||||
HREF_WIND_OSCILLATION = '/wind/oscillation/vs/0' # swing_mode fallback
|
||||
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
|
||||
HREF_TEMPS_VS = '/temperatures/vs/0' # vendor temp fallback (items[] array)
|
||||
|
||||
CLIMATE_CONSUMED_HREFS = [
|
||||
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
|
||||
HREF_TEMP_CONTROL, HREF_TEMPS_VS, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION,
|
||||
HREF_CONVENIENT,
|
||||
HREF_WIND_OSCILLATION, HREF_CONVENIENT,
|
||||
]
|
||||
|
||||
|
||||
@@ -177,6 +183,15 @@ def _climate_write(payload, rep, href=None):
|
||||
return (['wind', 'strength', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
if kind == 'swing':
|
||||
return (['wind', 'direction', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
if kind == 'oscillation':
|
||||
# value is the HA swing_mode string ('off'/'vertical'/'horizontal'/
|
||||
# 'both'); both axes are independent Swing|Fix toggles on this
|
||||
# resource, so one HA value maps to a pair of fields written
|
||||
# together (see climate.py's oscillation fallback).
|
||||
return (['wind', 'oscillation', 'vs', '0'], {
|
||||
'vertical': 'Swing' if value in ('vertical', 'both') else 'Fix',
|
||||
'horizontal': 'Swing' if value in ('horizontal', 'both') else 'Fix',
|
||||
})
|
||||
if kind == 'preset':
|
||||
return (['mode', 'convenient', 'vs', '0'], {'x.com.samsung.da.modes': value})
|
||||
return None
|
||||
@@ -299,6 +314,31 @@ CURRENT_LIMIT = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# Overload-response setting (issue #126, TP1X_DA-AC-RAC-01011 WindFree
|
||||
# variant): `operation` toggles the feature and `mode` picks 'Alarm' vs
|
||||
# 'PowerSaving' out of `supportedModes`, with a `savingTime` duration
|
||||
# ('20'/'40'/'60' minutes) alongside. Plausible shape (compressor-overload
|
||||
# alarm vs. automatic power throttling), but nothing in the dump confirms
|
||||
# the exact behavioral difference between the two modes or whether writing
|
||||
# 'operation' is safe on live HVAC hardware -- exposed read-only per the
|
||||
# 'don't guess' rule, same precedent as CURRENT_LIMIT above.
|
||||
ANOMALY_LOAD = Capability(
|
||||
href='/anomalyload/vs/0',
|
||||
poll_tier='cold',
|
||||
entities=(
|
||||
BinarySensorDesc(key='overload_protection_active', field='operation',
|
||||
icon='mdi:flash-alert',
|
||||
entity_category='diagnostic',
|
||||
value_fn=lambda v: v == 'On'),
|
||||
SensorDesc(key='overload_protection_mode', field='mode',
|
||||
device_class='enum',
|
||||
options=('alarm', 'powersaving'),
|
||||
translation_key='overload_protection_mode',
|
||||
icon='mdi:flash-alert', entity_category='diagnostic',
|
||||
value_fn=lambda v: v.lower() if isinstance(v, str) else v),
|
||||
),
|
||||
)
|
||||
|
||||
# The climate entity already surfaces current_temperature as a card
|
||||
# attribute, but that's not enough for history graphs/automations/
|
||||
# statistics -- issue #75 asked for a standalone sensor. Same OCF-standard-
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"current_limit_enabled": {
|
||||
"name": "Current limit enabled"
|
||||
},
|
||||
"overload_protection_active": {
|
||||
"name": "Overload protection active"
|
||||
},
|
||||
"cycle_active": {
|
||||
"name": "Running"
|
||||
},
|
||||
@@ -515,6 +518,13 @@
|
||||
"current_limit_level": {
|
||||
"name": "Current limit level"
|
||||
},
|
||||
"overload_protection_mode": {
|
||||
"name": "Overload protection mode",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"powersaving": "Power saving"
|
||||
}
|
||||
},
|
||||
"current_temp_c": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
"current_limit_enabled": {
|
||||
"name": "Stroombegrenzing ingeschakeld"
|
||||
},
|
||||
"overload_protection_active": {
|
||||
"name": "Overbelastingsbeveiliging actief"
|
||||
},
|
||||
"cycle_active": {
|
||||
"name": "Actief"
|
||||
},
|
||||
@@ -515,6 +518,13 @@
|
||||
"current_limit_level": {
|
||||
"name": "Niveau stroombegrenzing"
|
||||
},
|
||||
"overload_protection_mode": {
|
||||
"name": "Modus overbelastingsbeveiliging",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"powersaving": "Energiebesparing"
|
||||
}
|
||||
},
|
||||
"current_temp_c": {
|
||||
"name": "Temperatuur"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/aisleep/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.displayNightMode": "Off",
|
||||
"x.com.samsung.da.elapsedTime": "0",
|
||||
"x.com.samsung.da.requestFeedback": "Off",
|
||||
"x.com.samsung.da.resultFeedback": 0,
|
||||
"x.com.samsung.da.statusFeedback": "Idle",
|
||||
"x.com.samsung.da.sleepTime": "14002200"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-27T11:59:06",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.alarms"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/anomalyload/vs/0",
|
||||
"rep": {
|
||||
"operation": "Off",
|
||||
"mode": "Alarm",
|
||||
"supportedModes": [
|
||||
"Alarm",
|
||||
"PowerSaving"
|
||||
],
|
||||
"savingTime": "20",
|
||||
"supportedSavingTime": [
|
||||
"20",
|
||||
"40",
|
||||
"60"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "000000A0012C01610CC90C0F0000",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": [
|
||||
"PRODUCT_GLOBAL",
|
||||
"AI_GLOBAL_COOLONLY_4.0",
|
||||
"AI_3.0",
|
||||
"Auto_To_AI",
|
||||
"ENERGY_2.0",
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"DR",
|
||||
"SingleCommand_1"
|
||||
],
|
||||
"InstalledRoomSize": "0.0",
|
||||
"SkipInstalledRoomSize": "false",
|
||||
"RoomSizeMenuNumber": "Manual"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.softwarereset": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativeDate": "1785153515",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativePower": "47536",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePowerType": "total"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+08:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/filter/airdustfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterCapacity": "180",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": [
|
||||
"replaceable",
|
||||
"washable"
|
||||
],
|
||||
"x.com.samsung.da.filterStatus": "normal",
|
||||
"x.com.samsung.da.filterUsage": "44",
|
||||
"x.com.samsung.da.filterDesiredUsage": "180",
|
||||
"x.com.samsung.da.supportedFilterDesiredUsage": [
|
||||
"180",
|
||||
"300",
|
||||
"500",
|
||||
"700"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/humidity/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.humidity": "0",
|
||||
"x.com.samsung.da.fivepercentHumidity": "46"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-AC-RAC-01011_0000|10281241|6001051A001911018E0049300002A900",
|
||||
"x.com.samsung.da.description": "TP1X_DA-AC-RAC-01011_0000",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.serialNumOption": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02825A260401",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02812A25111800,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "02813A25101500,FFFFFFFFFFFFFF"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.newVersionAvailable": "0",
|
||||
"x.com.samsung.da.number": "FFFFFFFFFFFFFF,FFFFFFFFFFFFFF"
|
||||
}
|
||||
],
|
||||
"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": "AR2",
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/keepnormalstate/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.keepnormal": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/light/vs/0",
|
||||
"rep": {
|
||||
"mode": "On",
|
||||
"supportedModes": [
|
||||
"On",
|
||||
"Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mds/absencemonitoring/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/mds/absencestate/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/mode/convenient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "Off",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Off",
|
||||
"Sleep",
|
||||
"Quiet",
|
||||
"Smart",
|
||||
"Speed",
|
||||
"Nano",
|
||||
"NanoSleep",
|
||||
"DryComfort"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": [
|
||||
"Cool"
|
||||
],
|
||||
"workingMode": "Cool",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"Auto",
|
||||
"Cool",
|
||||
"Dry",
|
||||
"Fan"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"OptionCode_56416",
|
||||
"Sleep_0",
|
||||
"SleepRemainTime_0",
|
||||
"ArtificialWorking_Off",
|
||||
"ComfortAICooling_Off",
|
||||
"AiTempChanged_Off",
|
||||
"AiTemp_240",
|
||||
"Volume_100",
|
||||
"DiagnosisAI_Off",
|
||||
"ProgressDiagnosisAI_0",
|
||||
"ResultDiagnosisAI_Normal",
|
||||
"SmartCoolClean_Off",
|
||||
"ProgressSmartClean_0",
|
||||
"OutDoorVentil_Off",
|
||||
"InDoorVentil_Off",
|
||||
"FreezeAlarmSetting_Off",
|
||||
"DesiredFreezeAlarm_240",
|
||||
"WashAlarm_Off",
|
||||
"ExtendOptionCode_246413",
|
||||
"UpdateAllow_NotAllowed",
|
||||
"DurationOn_0",
|
||||
"WelcomeCoolingState_Off"
|
||||
],
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/autoclean/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.status": "Stop",
|
||||
"x.com.samsung.da.supportedStatus": [
|
||||
"Start",
|
||||
"Stop"
|
||||
],
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.settingStatus": "Off",
|
||||
"x.com.samsung.da.supportedSettingStatus": [
|
||||
"On",
|
||||
"Off"
|
||||
],
|
||||
"defaultSettingStatus": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/option/muteonce/vs/0",
|
||||
"rep": {
|
||||
"muteonce": "Off",
|
||||
"rt": [
|
||||
"x.com.samsung.da.option.muteonce"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "Off",
|
||||
"operationNumber": "2",
|
||||
"displayCondition": "Enable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/reserverulesets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "8EFFFFFFFF101E121EFFFFFFFFFFFF000001001F0001000000000000009C00FFFF1E00",
|
||||
"x.com.samsung.da.id": "RAC",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/selfcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.start": "Cancel",
|
||||
"x.com.samsung.da.status": "Ready",
|
||||
"x.com.samsung.da.progress": "0",
|
||||
"x.com.samsung.da.result": "NA",
|
||||
"x.com.samsung.da.supportedActions": [
|
||||
"Start",
|
||||
"Cancel"
|
||||
],
|
||||
"x.com.samsung.da.error": [
|
||||
"DA_ERROR_NONE"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/control/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.increment": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Temperature",
|
||||
"x.com.samsung.da.current": "29.0",
|
||||
"x.com.samsung.da.desired": "18.0",
|
||||
"x.com.samsung.da.minimum": "16",
|
||||
"x.com.samsung.da.maximum": "30",
|
||||
"x.com.samsung.da.increment": "1",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/oscillation/vs/0",
|
||||
"rep": {
|
||||
"vertical": "Fix",
|
||||
"supportedVertical": [
|
||||
"Swing",
|
||||
"Fix"
|
||||
],
|
||||
"verticalAngle": "4",
|
||||
"supportedVerticalAngle": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6"
|
||||
],
|
||||
"horizontal": "Fix",
|
||||
"supportedHorizontal": [
|
||||
"Swing",
|
||||
"Fix"
|
||||
],
|
||||
"horizontalSwingMode": "AllSwing",
|
||||
"supportedHorizontalSwingMode": [
|
||||
"AllSwing",
|
||||
"LeftSwing",
|
||||
"RightSwing"
|
||||
],
|
||||
"horizontalAngle": "3",
|
||||
"supportedHorizontalAngle": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wind/strength/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modes": "1",
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4"
|
||||
],
|
||||
"x.com.samsung.da.modesName": [
|
||||
"Auto",
|
||||
"Low",
|
||||
"Mid",
|
||||
"High",
|
||||
"Turbo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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 Air conditioner",
|
||||
"osVersion": "4.0"
|
||||
},
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "ARA-WW-TP1-26-AR6X00MG",
|
||||
"versions": [
|
||||
"11260401"
|
||||
],
|
||||
"visVersion": "260401"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "845241131741FFFFFFFF",
|
||||
"versions": [
|
||||
"25102900",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251029"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210281241FFFFFFFF",
|
||||
"versions": [
|
||||
"25111800",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251118"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "045210281341FFFFFFFF",
|
||||
"versions": [
|
||||
"25101500",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "251015"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "0452FFFFFFFFFFFFFFFF",
|
||||
"versions": [
|
||||
"FFFFFFFF",
|
||||
"FFFFFFFF"
|
||||
],
|
||||
"visVersion": "999999"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Kuala_Lumpur",
|
||||
"offset": "+08: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": "K9 Unit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/0",
|
||||
"rep": {
|
||||
"temperature": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_filter_status",
|
||||
"air_filter_usage",
|
||||
"alarm_code",
|
||||
"auto_clean",
|
||||
"climate",
|
||||
"current_temperature_c",
|
||||
"display_light",
|
||||
"energy_kwh",
|
||||
"energy_saved_kwh",
|
||||
"firmware_update",
|
||||
"humidity",
|
||||
"mute_once",
|
||||
"overload_protection_active",
|
||||
"overload_protection_mode",
|
||||
"selfcheck_error",
|
||||
"selfcheck_result",
|
||||
"selfcheck_status"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for the /wind/oscillation/vs/0 swing fallback helper in climate.py
|
||||
(issue #126).
|
||||
|
||||
`_oscillation_swing` is a pure module-level function, like `_temps_vs_item`
|
||||
in test_climate_temperature_fallback.py -- testable directly without a
|
||||
coordinator/entity. This covers the fallback-selection logic that the
|
||||
registry-level "no unbound hrefs" test in test_golden_regression.py doesn't
|
||||
reach: newer WindFree firmware reports no /wind/direction/vs/0 at all, so
|
||||
swing_mode/swing_modes would silently be None/empty without this fallback.
|
||||
"""
|
||||
from custom_components.localthings.climate import _oscillation_swing
|
||||
|
||||
|
||||
def test_oscillation_swing_off_when_both_fixed():
|
||||
assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Fix'}) == 'off'
|
||||
|
||||
|
||||
def test_oscillation_swing_vertical_only():
|
||||
assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Fix'}) == 'vertical'
|
||||
|
||||
|
||||
def test_oscillation_swing_horizontal_only():
|
||||
assert _oscillation_swing({'vertical': 'Fix', 'horizontal': 'Swing'}) == 'horizontal'
|
||||
|
||||
|
||||
def test_oscillation_swing_both():
|
||||
assert _oscillation_swing({'vertical': 'Swing', 'horizontal': 'Swing'}) == 'both'
|
||||
|
||||
|
||||
def test_oscillation_swing_none_when_resource_absent():
|
||||
"""Boards with /wind/direction/vs/0 instead don't populate this
|
||||
resource at all -- callers must fall through cleanly, not raise."""
|
||||
assert _oscillation_swing({}) is None
|
||||
@@ -545,6 +545,29 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_ara_ww_tp1_22(
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_airconditioner_windfree_oscillation():
|
||||
"""TP1X_DA-AC-RAC-01011_0000 Bespoke AI WindFree Deluxe (model
|
||||
AR60H10D1JWNME, issue #126) -- the same board family as the
|
||||
airconditioner_tp1x_da_ac_rac_01011 fixture, but a newer firmware
|
||||
variant reporting no /wind/direction/vs/0 at all: swing lives on the
|
||||
2-axis /wind/oscillation/vs/0 resource instead (airconditioner.
|
||||
HREF_WIND_OSCILLATION, climate.py's oscillation fallback), and it adds
|
||||
an /anomalyload/vs/0 overload-response resource (airconditioner.
|
||||
ANOMALY_LOAD, read-only per the 'don't guess' rule). Binds cleanly
|
||||
with zero unbound hrefs."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('airconditioner_windfree_oscillation')
|
||||
golden = json.loads(
|
||||
(GOLDEN / 'airconditioner_windfree_oscillation.json').read_text()
|
||||
)
|
||||
state_keys = _new_state_keys('airconditioner_windfree_oscillation', 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 = {
|
||||
|
||||
Reference in new issue
Block a user