refactor: tidy the AC climate entity (/simplify pass)
Quality-only cleanups on the air-conditioner support, no behavior change: - climate.py: reuse common.normalize_temp_unit for the C/F unit read (also handles the "Celsius"/"Fahrenheit" long forms); collapse the three fan/swing/preset read properties into _read_mode/_read_modes and the three write setters into _set_mapped, removing the copy-paste. - Single source of truth for the climate-consumed hrefs: they lived both as constants in climate.py and as a list in airconditioner.py. Declare them once in airconditioner.py (HREF_* + CLIMATE_CONSUMED_HREFS, which also builds the COVERAGE caps) and import them into climate.py, so a new sibling read can't drift out of sync with its coverage entry.
This commit is contained in:
1 parent
c0298d57d9
commit
f4b4a04628
2 files changed
+74
-52
No files matched your search
@@ -30,24 +30,27 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .registry.entities import ClimateDesc
|
||||
# The AC's canonical resource hrefs live in the capability module (the single
|
||||
# source of truth shared with its COVERAGE caps); power prefers the OCF-standard
|
||||
# href, falling back to the vendor one, mirroring common.POWER_GENERIC /
|
||||
# POWER_VS_FALLBACK.
|
||||
from .registry.capabilities.airconditioner import (
|
||||
HREF_MODE as MODE_HREF,
|
||||
HREF_POWER as POWER_HREF,
|
||||
HREF_POWER_VS as POWER_VS_HREF,
|
||||
HREF_TEMP_CURRENT as TEMP_CURRENT_HREF,
|
||||
HREF_TEMP_DESIRED as TEMP_DESIRED_HREF,
|
||||
HREF_TEMP_CONTROL as TEMP_CONTROL_HREF,
|
||||
HREF_WIND_STRENGTH as WIND_STRENGTH_HREF,
|
||||
HREF_WIND_DIRECTION as WIND_DIRECTION_HREF,
|
||||
HREF_CONVENIENT as CONVENIENT_HREF,
|
||||
)
|
||||
from .registry.capabilities.common import normalize_temp_unit
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LocalThingsCoordinator
|
||||
from .entity import LocalThingsEntity, _is_included
|
||||
|
||||
# Sibling OCF resources the climate entity reads (the primary bound href is
|
||||
# /mode/vs/0). Power prefers the OCF-standard href, falling back to the vendor
|
||||
# one, mirroring common.POWER_GENERIC / POWER_VS_FALLBACK.
|
||||
POWER_HREF = '/power/0'
|
||||
POWER_VS_HREF = '/power/vs/0'
|
||||
MODE_HREF = '/mode/vs/0'
|
||||
TEMP_CURRENT_HREF = '/temperature/current/0'
|
||||
TEMP_DESIRED_HREF = '/temperature/desired/0'
|
||||
TEMP_CONTROL_HREF = '/temperature/control/vs/0'
|
||||
WIND_STRENGTH_HREF = '/wind/strength/vs/0'
|
||||
WIND_DIRECTION_HREF = '/wind/direction/vs/0'
|
||||
CONVENIENT_HREF = '/mode/convenient/vs/0'
|
||||
|
||||
_MODES_FIELD = 'x.com.samsung.da.modes'
|
||||
_SUPPORTED_FIELD = 'x.com.samsung.da.supportedModes'
|
||||
|
||||
@@ -159,12 +162,22 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
def _supported(self, href: str) -> list[str]:
|
||||
return list(self._rep(href).get(_SUPPORTED_FIELD) or [])
|
||||
|
||||
def _read_mode(self, href: str, mapping: dict):
|
||||
"""Current mode of a wind/convenient resource, mapped to its HA value."""
|
||||
return mapping.get(_first(self._rep(href).get(_MODES_FIELD)))
|
||||
|
||||
def _read_modes(self, href: str, mapping: dict) -> list[str]:
|
||||
"""Supported modes of a resource, mapped to HA values (unknowns dropped)."""
|
||||
return [mapping[c] for c in self._supported(href) if c in mapping]
|
||||
|
||||
# -- temperature --------------------------------------------------------
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
units = str(self._rep(TEMP_DESIRED_HREF).get('units', 'C')).upper()
|
||||
return UnitOfTemperature.FAHRENHEIT if units.startswith('F') else UnitOfTemperature.CELSIUS
|
||||
raw = self._rep(TEMP_DESIRED_HREF).get('units')
|
||||
return (UnitOfTemperature.FAHRENHEIT
|
||||
if normalize_temp_unit(raw, '°C') == '°F'
|
||||
else UnitOfTemperature.CELSIUS)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -214,30 +227,27 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
return _DEVICE_TO_FAN.get(_first(self._rep(WIND_STRENGTH_HREF).get(_MODES_FIELD)))
|
||||
return self._read_mode(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str]:
|
||||
return [_DEVICE_TO_FAN[c] for c in self._supported(WIND_STRENGTH_HREF)
|
||||
if c in _DEVICE_TO_FAN]
|
||||
return self._read_modes(WIND_STRENGTH_HREF, _DEVICE_TO_FAN)
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
return _DEVICE_TO_SWING.get(_first(self._rep(WIND_DIRECTION_HREF).get(_MODES_FIELD)))
|
||||
return self._read_mode(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
|
||||
@property
|
||||
def swing_modes(self) -> list[str]:
|
||||
return [_DEVICE_TO_SWING[c] for c in self._supported(WIND_DIRECTION_HREF)
|
||||
if c in _DEVICE_TO_SWING]
|
||||
return self._read_modes(WIND_DIRECTION_HREF, _DEVICE_TO_SWING)
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
return _DEVICE_TO_PRESET.get(_first(self._rep(CONVENIENT_HREF).get(_MODES_FIELD)))
|
||||
return self._read_mode(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str]:
|
||||
return [_DEVICE_TO_PRESET[c] for c in self._supported(CONVENIENT_HREF)
|
||||
if c in _DEVICE_TO_PRESET]
|
||||
return self._read_modes(CONVENIENT_HREF, _DEVICE_TO_PRESET)
|
||||
|
||||
# -- writes -------------------------------------------------------------
|
||||
|
||||
@@ -263,17 +273,17 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity):
|
||||
async def async_turn_off(self) -> None:
|
||||
await self.coordinator.async_send_command(self._bound, ('power', False))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
device = _FAN_TO_DEVICE.get(fan_mode)
|
||||
async def _set_mapped(self, kind: str, mapping: dict, value: str) -> None:
|
||||
"""Map an HA fan/swing/preset value back to its device code and write it."""
|
||||
device = mapping.get(value)
|
||||
if device is not None:
|
||||
await self.coordinator.async_send_command(self._bound, ('fan', device))
|
||||
await self.coordinator.async_send_command(self._bound, (kind, device))
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
await self._set_mapped('fan', _FAN_TO_DEVICE, fan_mode)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
device = _SWING_TO_DEVICE.get(swing_mode)
|
||||
if device is not None:
|
||||
await self.coordinator.async_send_command(self._bound, ('swing', device))
|
||||
await self._set_mapped('swing', _SWING_TO_DEVICE, swing_mode)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
device = _PRESET_TO_DEVICE.get(preset_mode)
|
||||
if device is not None:
|
||||
await self.coordinator.async_send_command(self._bound, ('preset', device))
|
||||
await self._set_mapped('preset', _PRESET_TO_DEVICE, preset_mode)
|
||||
@@ -17,6 +17,29 @@ by_type registry.
|
||||
from ..capability import Capability
|
||||
from ..entities import ClimateDesc, SensorDesc, SwitchDesc
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Canonical AC resource hrefs. The climate entity (climate.py) binds the
|
||||
# primary HREF_MODE via CLIMATE below and reads the CLIMATE_CONSUMED_HREFS
|
||||
# siblings off the coordinator snapshot; those siblings are marked covered
|
||||
# (no-entity caps) so discover() reports no gap. Declared once here and
|
||||
# imported by climate.py, so a new sibling read can't drift out of sync with
|
||||
# its coverage entry.
|
||||
# ---------------------------------------------------------------------------
|
||||
HREF_MODE = '/mode/vs/0' # primary (bound by CLIMATE)
|
||||
HREF_POWER = '/power/0' # on/off -> HVACMode.OFF / TURN_ON/OFF
|
||||
HREF_POWER_VS = '/power/vs/0' # vendor fallback for on/off
|
||||
HREF_TEMP_CURRENT = '/temperature/current/0' # current_temperature
|
||||
HREF_TEMP_DESIRED = '/temperature/desired/0' # target_temperature (write target)
|
||||
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
|
||||
HREF_CONVENIENT = '/mode/convenient/vs/0' # preset_mode
|
||||
|
||||
CLIMATE_CONSUMED_HREFS = [
|
||||
HREF_POWER, HREF_POWER_VS, HREF_TEMP_CURRENT, HREF_TEMP_DESIRED,
|
||||
HREF_TEMP_CONTROL, HREF_WIND_STRENGTH, HREF_WIND_DIRECTION, HREF_CONVENIENT,
|
||||
]
|
||||
|
||||
|
||||
def _num(v):
|
||||
try:
|
||||
@@ -71,7 +94,7 @@ def _climate_write(payload, rep, href=None):
|
||||
|
||||
|
||||
CLIMATE = Capability(
|
||||
href='/mode/vs/0',
|
||||
href=HREF_MODE,
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
ClimateDesc(key='climate', translation_key='airconditioner',
|
||||
@@ -124,24 +147,13 @@ AIR_FILTER = Capability(
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AC-scoped coverage: hrefs consumed by the composite climate entity, and
|
||||
# vendor duplicates / all-zero-ambiguous / plumbing resources. These are NOT in
|
||||
# the global ignored.IGNORED because several of them (/mode/vs/0 handled above,
|
||||
# /temperatures/vs/0, /humidity/*) collide with other families' schemas. A
|
||||
# no-entity Capability still marks the href as bound so discover() reports no
|
||||
# coverage gap.
|
||||
# AC-scoped coverage: the CLIMATE_CONSUMED_HREFS above (read by the climate
|
||||
# entity) plus vendor duplicates / all-zero-ambiguous / plumbing resources.
|
||||
# These are NOT in the global ignored.IGNORED because several of them
|
||||
# (/mode/vs/0 handled above, /temperatures/vs/0, /humidity/*) collide with
|
||||
# other families' schemas. A no-entity Capability still marks the href as
|
||||
# bound so discover() reports no coverage gap.
|
||||
# ---------------------------------------------------------------------------
|
||||
_CONSUMED_BY_CLIMATE = [
|
||||
'/power/0', # on/off -> climate HVACMode.OFF / TURN_ON/OFF
|
||||
'/power/vs/0', # vendor fallback for on/off
|
||||
'/temperature/current/0', # climate current_temperature
|
||||
'/temperature/desired/0', # climate target_temperature (write target)
|
||||
'/temperature/control/vs/0', # climate target_temperature_step
|
||||
'/wind/strength/vs/0', # climate fan_mode
|
||||
'/wind/direction/vs/0', # climate swing_mode
|
||||
'/mode/convenient/vs/0', # climate preset_mode
|
||||
]
|
||||
|
||||
_AC_IGNORED = [
|
||||
# Vendor superset that duplicates the OCF /temperature/current+desired pair.
|
||||
'/temperatures/vs/0',
|
||||
@@ -155,4 +167,4 @@ _AC_IGNORED = [
|
||||
]
|
||||
|
||||
# Built as bare no-entity caps; folded into the AC registry (not global).
|
||||
COVERAGE = [Capability(href=h) for h in (_CONSUMED_BY_CLIMATE + _AC_IGNORED)]
|
||||
COVERAGE = [Capability(href=h) for h in (CLIMATE_CONSUMED_HREFS + _AC_IGNORED)]
|
||||
Reference in new issue
Block a user