diff --git a/.python-version b/.python-version index d2c96c0..92536a9 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.3 +3.12.0 diff --git a/custom_components/daikinone/climate.py b/custom_components/daikinone/climate.py index 7aa6777..955f4b8 100644 --- a/custom_components/daikinone/climate.py +++ b/custom_components/daikinone/climate.py @@ -40,7 +40,7 @@ async def async_setup_entry( entities = [ DaikinOneThermostat( - ClimateEntityDescription(key=device.id, name=None), + ClimateEntityDescription(key=device.id, has_entity_name=True, name=None), data, device, ) @@ -68,94 +68,21 @@ def __init__( self._data = data self._thermostat = thermostat - @property - def unique_id(self): - """Return a unique identifier for this sensor.""" - return f"{self._thermostat.id}-climate" - - @property - def device_info(self) -> DeviceInfo | None: - """Return device information for this sensor.""" - - return DeviceInfo( + self._attr_unique_id = f"{self._thermostat.id}-climate" + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._thermostat.id)}, name=f"{self._thermostat.name} Thermostat", manufacturer=MANUFACTURER, model=self._thermostat.model, sw_version=self._thermostat.firmware_version, ) + self._attr_hvac_modes = self.get_hvac_modes() - @property - def available(self): - """Return if device is available.""" - return self._thermostat.online - - @property - def supported_features(self): - return ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - - @property - def temperature_unit(self): - return UnitOfTemperature.CELSIUS - - @property - def current_temperature(self) -> float | None: - return self._thermostat.indoor_temperature.celsius - - @property - def target_temperature(self) -> float | None: - match self._thermostat.mode: - case DaikinThermostatMode.HEAT | DaikinThermostatMode.AUX_HEAT: - return self._thermostat.set_point_heat.celsius - case DaikinThermostatMode.COOL: - return self._thermostat.set_point_cool.celsius - case _: - pass - - return None - - @property - def target_temperature_low(self) -> float | None: - match self._thermostat.mode: - case DaikinThermostatMode.AUTO: - return self._thermostat.set_point_heat.celsius - case _: - pass - - return None - - @property - def target_temperature_high(self) -> float | None: - match self._thermostat.mode: - case DaikinThermostatMode.AUTO: - return self._thermostat.set_point_cool.celsius - case _: - pass - - return None - - @property - def min_temp(self) -> float: - # these should be the same but just in case, take the larger of the two for the min - return max( - self._thermostat.set_point_heat_min.celsius, - self._thermostat.set_point_cool_min.celsius, - ) - - @property - def max_temp(self) -> float: - # these should be the same but just in case, take the smaller of the two for the max - return min( - self._thermostat.set_point_heat_max.celsius, - self._thermostat.set_point_cool_max.celsius, - ) - - @property - def current_humidity(self) -> int: - return self._thermostat.indoor_humidity - - @property - def hvac_modes(self) -> list[HVACMode]: + def get_hvac_modes(self) -> list[HVACMode]: modes: list[HVACMode] = [] if ( @@ -173,34 +100,6 @@ def hvac_modes(self) -> list[HVACMode]: return modes - @property - def hvac_mode(self): - match self._thermostat.mode: - case DaikinThermostatMode.AUTO: - return HVACMode.HEAT_COOL - case DaikinThermostatMode.HEAT: - return HVACMode.HEAT - case DaikinThermostatMode.COOL: - return HVACMode.COOL - case DaikinThermostatMode.AUX_HEAT: - return HVACMode.HEAT - case DaikinThermostatMode.OFF: - return HVACMode.OFF - - @property - def hvac_action(self): - match self._thermostat.status: - case DaikinThermostatStatus.HEATING: - return HVACAction.HEATING - case DaikinThermostatStatus.COOLING: - return HVACAction.COOLING - case DaikinThermostatStatus.CIRCULATING_AIR: - return HVACAction.FAN - case DaikinThermostatStatus.DRYING: - return HVACAction.DRYING - case DaikinThermostatStatus.IDLE: - return HVACAction.IDLE - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" target_mode: DaikinThermostatMode @@ -312,6 +211,77 @@ async def async_update(self, no_throttle: bool = False) -> None: await self._data.update(no_throttle=no_throttle) self._thermostat = self._data.daikin.get_thermostat(self._thermostat.id) + # update entity data + # + self._attr_available = self._thermostat.online + self._attr_current_temperature = self._thermostat.indoor_temperature.celsius + self._attr_current_humidity = self._thermostat.indoor_humidity + + # hvac mode + # + match self._thermostat.mode: + case DaikinThermostatMode.AUTO: + self._attr_hvac_mode = HVACMode.HEAT_COOL + case DaikinThermostatMode.HEAT: + self._attr_hvac_mode = HVACMode.HEAT + case DaikinThermostatMode.COOL: + self._attr_hvac_mode = HVACMode.COOL + case DaikinThermostatMode.AUX_HEAT: + self._attr_hvac_mode = HVACMode.HEAT + case DaikinThermostatMode.OFF: + self._attr_hvac_mode = HVACMode.OFF + + # hvac action + # + match self._thermostat.status: + case DaikinThermostatStatus.HEATING: + self._attr_hvac_action = HVACAction.HEATING + case DaikinThermostatStatus.COOLING: + self._attr_hvac_action = HVACAction.COOLING + case DaikinThermostatStatus.CIRCULATING_AIR: + self._attr_hvac_action = HVACAction.FAN + case DaikinThermostatStatus.DRYING: + self._attr_hvac_action = HVACAction.DRYING + case DaikinThermostatStatus.IDLE: + self._attr_hvac_action = HVACAction.IDLE + + # target temperature + # + match self._thermostat.mode: + case DaikinThermostatMode.HEAT | DaikinThermostatMode.AUX_HEAT: + self._attr_target_temperature = self._thermostat.set_point_heat.celsius + case DaikinThermostatMode.COOL: + self._attr_target_temperature = self._thermostat.set_point_cool.celsius + case _: + pass + + # target temperature range + # + match self._thermostat.mode: + case DaikinThermostatMode.AUTO: + self._attr_target_temperature_low = self._thermostat.set_point_heat.celsius + case _: + pass + match self._thermostat.mode: + case DaikinThermostatMode.AUTO: + self._attr_target_temperature_high = self._thermostat.set_point_cool.celsius + case _: + pass + + # temperature bounds + # + + # these should be the same but just in case, take the larger of the two for the min + self._attr_min_temp = max( + self._thermostat.set_point_heat_min.celsius, + self._thermostat.set_point_cool_min.celsius, + ) + # these should be the same but just in case, take the smaller of the two for the max + self._attr_max_temp = min( + self._thermostat.set_point_heat_max.celsius, + self._thermostat.set_point_cool_max.celsius, + ) + async def update_state_optimistically( self, update: Callable[[DaikinThermostat], None], check: Callable[[DaikinThermostat], bool] ) -> None: diff --git a/custom_components/daikinone/config_flow.py b/custom_components/daikinone/config_flow.py index 467316c..a912841 100644 --- a/custom_components/daikinone/config_flow.py +++ b/custom_components/daikinone/config_flow.py @@ -22,7 +22,7 @@ def schema(self): return vol.Schema({vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}) async def async_step_user(self, user_input: dict[str, Any] | None = None): - errors = {} + errors: dict[str, str] = {} if user_input is not None: email = user_input[CONF_EMAIL] diff --git a/custom_components/daikinone/daikinone.py b/custom_components/daikinone/daikinone.py index 12101b1..8553201 100644 --- a/custom_components/daikinone/daikinone.py +++ b/custom_components/daikinone/daikinone.py @@ -202,7 +202,7 @@ async def set_thermostat_home_set_points( if not heat and not cool: raise ValueError("At least one of heat or cool set points must be set") - payload = {} + payload: dict[str, Any] = {} if heat: payload["hspHome"] = heat.celsius if cool: diff --git a/custom_components/daikinone/sensor.py b/custom_components/daikinone/sensor.py index 337c086..cfb043e 100644 --- a/custom_components/daikinone/sensor.py +++ b/custom_components/daikinone/sensor.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, TypeVar, Generic +from typing import Callable, Generic, TypeVar from homeassistant.components.sensor import SensorEntity, SensorEntityDescription, SensorDeviceClass, SensorStateClass from homeassistant.config_entries import ConfigEntry @@ -547,8 +547,6 @@ async def async_setup_entry( class DaikinOneSensor(SensorEntity, Generic[D]): - _state: StateType = None - def __init__( self, description: SensorEntityDescription, data: DaikinOneData, device: D, attribute: Callable[[D], StateType] ) -> None: @@ -559,13 +557,10 @@ def __init__( self._device: D = device self._attribute = attribute - @property - def unique_id(self): - """Return a unique identifier for this sensor.""" - return f"{self._device.id}-{self.name}" + self._attr_unique_id = f"{self._device.id}-{self.name}" + self._attr_device_info = self.get_device_info() - @property - def device_info(self) -> DeviceInfo | None: + def get_device_info(self) -> DeviceInfo | None: """Return device information for this sensor.""" info = DeviceInfo( @@ -591,11 +586,6 @@ def device_parent(self) -> str | None: """Return the name of the device.""" return None - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - class DaikinOneThermostatSensor(DaikinOneSensor[DaikinThermostat]): def __init__( @@ -615,7 +605,7 @@ async def async_update(self) -> None: """Get the latest state of the sensor.""" await self._data.update() self._device = self._data.daikin.get_thermostat(self._device.id) - self._state = self._attribute(self._device) + self._attr_native_value = self._attribute(self._device) E = TypeVar("E", bound=DaikinEquipment) @@ -637,4 +627,4 @@ async def async_update(self) -> None: await self._data.update() thermostat = self._data.daikin.get_thermostat(self._device.thermostat_id) self._device = thermostat.equipment[self._device.id] - self._state = self._attribute(self._device) + self._attr_native_value = self._attribute(self._device) diff --git a/pyproject.toml b/pyproject.toml index 3b99af9..285abae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,21 @@ [project] name = "ha-daikinone" version = "0.1.0" -description = "Add a short description here" +description = "Daikin One+ integration for Home Assistant" authors = [ { name = "Zach Langbert", email = "zach.langbert@gmail.com" } ] -dependencies = ["homeassistant~=2023.6.1", "pydantic~=1.10.8", "backoff~=2.2.1"] +dependencies = ["homeassistant~=2024.1.0", "pydantic~=1.10.12", "backoff~=2.2.1"] readme = "README.md" -requires-python = ">= 3.11" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires-python = ">= 3.12" [tool.rye] managed = true dev-dependencies = [ - "setuptools~=67.8.0", - "ruff~=0.0.270", - "pyright~=1.1.314", - "black[d]~=23.3.0", + "setuptools>=69.0.3", + "ruff>=0.1.13", + "pyright>=1.1.347", + "black[d]>=23.12.1", ] [tool.rye.scripts] diff --git a/requirements-dev.lock b/requirements-dev.lock index a000096..f5d66e7 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,59 +7,78 @@ # all-features: false -e file:. -aiohttp==3.8.4 +aiofiles==23.2.1 +aiohttp==3.9.1 +aiohttp-cors==0.7.0 +aiohttp-fast-url-dispatcher==0.3.0 +aiohttp-zlib-ng==0.1.3 aiosignal==1.3.1 -anyio==3.6.2 +anyio==4.2.0 astral==2.2 -async-timeout==4.0.2 atomicwrites-homeassistant==1.4.1 -attrs==22.2.0 -awesomeversion==22.9.0 +attrs==23.1.0 +awesomeversion==23.11.0 backoff==2.2.1 bcrypt==4.0.1 -black==23.3.0 -certifi==2023.5.7 -cffi==1.15.1 -charset-normalizer==3.1.0 +black==23.12.1 +bleak==0.21.1 +bleak-retry-connector==3.4.0 +bluetooth-adapters==0.17.0 +bluetooth-auto-recovery==1.3.0 +bluetooth-data-tools==1.19.0 +btsocket==0.2.0 +certifi==2023.11.17 +cffi==1.16.0 +charset-normalizer==3.3.2 ciso8601==2.3.0 -click==8.1.3 -cryptography==40.0.2 -frozenlist==1.3.3 +click==8.1.7 +cryptography==41.0.7 +dbus-fast==2.21.0 +frozenlist==1.4.1 h11==0.14.0 -home-assistant-bluetooth==1.10.0 -homeassistant==2023.6.2 -httpcore==0.17.2 -httpx==0.24.1 -idna==3.4 +habluetooth==2.1.0 +home-assistant-bluetooth==1.12.0 +homeassistant==2024.1.3 +httpcore==1.0.2 +httpx==0.26.0 +idna==3.6 ifaddr==0.2.0 jinja2==3.1.2 -lru-dict==1.1.8 -markupsafe==2.1.2 +lru-dict==1.3.0 +mac-vendor-lookup==0.1.12 +markupsafe==2.1.3 multidict==6.0.4 mypy-extensions==1.0.0 nodeenv==1.8.0 -orjson==3.8.12 -packaging==23.1 -pathspec==0.11.1 -platformdirs==3.5.1 +orjson==3.9.9 +packaging==23.2 +pathspec==0.12.1 +platformdirs==4.1.0 pycparser==2.21 -pydantic==1.10.8 -pyjwt==2.7.0 -pyopenssl==23.1.0 -pyright==1.1.314 +pydantic==1.10.13 +pyjwt==2.8.0 +pyobjc-core==9.2 +pyobjc-framework-cocoa==9.2 +pyobjc-framework-corebluetooth==9.2 +pyobjc-framework-libdispatch==9.2 +pyopenssl==23.2.0 +pyric==0.1.6.3 +pyright==1.1.347 python-slugify==4.0.1 -pytz==2023.3 -pyyaml==6.0 +pytz==2023.3.post1 +pyyaml==6.0.1 requests==2.31.0 -ruff==0.0.270 +ruff==0.1.13 sniffio==1.3.0 text-unidecode==1.3 -typing-extensions==4.6.2 -ulid-transform==0.7.2 -urllib3==1.26.16 +typing-extensions==4.9.0 +ulid-transform==0.9.0 +urllib3==1.26.18 +usb-devices==0.4.5 voluptuous==0.13.1 voluptuous-serialize==2.6.0 -yarl==1.9.2 +yarl==1.9.4 +zlib-ng==0.4.0 # The following packages are considered to be unsafe in a requirements file: -pip==23.1.2 -setuptools==67.8.0 +pip==23.3.2 +setuptools==69.0.3 diff --git a/requirements.lock b/requirements.lock index 8efe290..4ee8f41 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,49 +7,69 @@ # all-features: false -e file:. -aiohttp==3.8.4 +aiofiles==23.2.1 +aiohttp==3.9.1 +aiohttp-cors==0.7.0 +aiohttp-fast-url-dispatcher==0.3.0 +aiohttp-zlib-ng==0.1.3 aiosignal==1.3.1 -anyio==3.6.2 +anyio==4.2.0 astral==2.2 -async-timeout==4.0.2 atomicwrites-homeassistant==1.4.1 -attrs==22.2.0 -awesomeversion==22.9.0 +attrs==23.1.0 +awesomeversion==23.11.0 backoff==2.2.1 bcrypt==4.0.1 -certifi==2023.5.7 -cffi==1.15.1 -charset-normalizer==3.1.0 +bleak==0.21.1 +bleak-retry-connector==3.4.0 +bluetooth-adapters==0.17.0 +bluetooth-auto-recovery==1.3.0 +bluetooth-data-tools==1.19.0 +btsocket==0.2.0 +certifi==2023.11.17 +cffi==1.16.0 +charset-normalizer==3.3.2 ciso8601==2.3.0 -cryptography==40.0.2 -frozenlist==1.3.3 +cryptography==41.0.7 +dbus-fast==2.21.0 +frozenlist==1.4.1 h11==0.14.0 -home-assistant-bluetooth==1.10.0 -homeassistant==2023.6.2 -httpcore==0.17.2 -httpx==0.24.1 -idna==3.4 +habluetooth==2.1.0 +home-assistant-bluetooth==1.12.0 +homeassistant==2024.1.3 +httpcore==1.0.2 +httpx==0.26.0 +idna==3.6 ifaddr==0.2.0 jinja2==3.1.2 -lru-dict==1.1.8 -markupsafe==2.1.2 +lru-dict==1.3.0 +mac-vendor-lookup==0.1.12 +markupsafe==2.1.3 multidict==6.0.4 -orjson==3.8.12 +orjson==3.9.9 +packaging==23.2 pycparser==2.21 -pydantic==1.10.8 -pyjwt==2.7.0 -pyopenssl==23.1.0 +pydantic==1.10.13 +pyjwt==2.8.0 +pyobjc-core==9.2 +pyobjc-framework-cocoa==9.2 +pyobjc-framework-corebluetooth==9.2 +pyobjc-framework-libdispatch==9.2 +pyopenssl==23.2.0 +pyric==0.1.6.3 python-slugify==4.0.1 -pytz==2023.3 -pyyaml==6.0 +pytz==2023.3.post1 +pyyaml==6.0.1 requests==2.31.0 sniffio==1.3.0 text-unidecode==1.3 -typing-extensions==4.6.2 -ulid-transform==0.7.2 -urllib3==1.26.16 +typing-extensions==4.9.0 +ulid-transform==0.9.0 +urllib3==1.26.18 +usb-devices==0.4.5 voluptuous==0.13.1 voluptuous-serialize==2.6.0 -yarl==1.9.2 +yarl==1.9.4 +zlib-ng==0.4.0 # The following packages are considered to be unsafe in a requirements file: -pip==23.1.2 +pip==23.3.2