diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..324f808 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +# E203 whitespace before ':' (disabled to improve interop with black) +# E501 line length > 79 characters +extend-ignore = E203,E501 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..3cce398 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,15 @@ +name: pre-commit + +on: + pull_request: + push: + branches-ignore: + - master + +jobs: + all-hooks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..4a0abcf --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,5 @@ +[settings] +; Align trailing comma behavior to black's behavior. +include_trailing_comma=True +; Vertical hanging indent. +multi_line_output=3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..57dc799 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-json + - id: check-merge-conflict + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort diff --git a/API_info.md b/API_info.md index b5e1254..1a16191 100644 --- a/API_info.md +++ b/API_info.md @@ -159,7 +159,7 @@ Night mode: Sensors: ``` -"equipmentStatus": the running state of the system, 1=cooling, 2=overcool dehumidifying, 3=heating, 4=fan, 5=idle, +"equipmentStatus": the running state of the system, 1=cooling, 2=overcool dehumidifying, 3=heating, 4=fan, 5=idle, “tempIndoor”: current indoor temperature, in C (thermostat measurement) “humIndoor”: current indoor humidity, in % (thermostat measurement) “tempOutdoor”: current outdoor temperature, in C (cloud-based) @@ -197,7 +197,7 @@ Sensors: "aq[In/Out]doorValue": AQI score "aqIndoorParticlesLevel": TBD "aqIndoorVOCLevel": TBD -"ctOutdoorAirTemperature": outdoor unit air temperature (measurement); needs to be divided by 10 and converted from Farenheit to Celcius. i.e., ((ctOutdoorAirTemperature / 10) - 32) * 5 / 9 +"ctOutdoorAirTemperature": outdoor unit air temperature (measurement); needs to be divided by 10 and converted from Farenheit to Celcius. i.e., ((ctOutdoorAirTemperature / 10) - 32) * 5 / 9 "ctOutdoorPower": outdoor unit power usage; multiply by 10 for Watts "ctIndoorPower": indoor unit power usage; usage TBD "ctIFCIndoorBlowerAirflow; furnace blower aiflow in CFM" diff --git a/custom_components/daikinskyport/__init__.py b/custom_components/daikinskyport/__init__.py index e70085f..da52530 100644 --- a/custom_components/daikinskyport/__init__.py +++ b/custom_components/daikinskyport/__init__.py @@ -1,38 +1,23 @@ """Daikin Skyport integration.""" -import os + from datetime import timedelta -from async_timeout import timeout -from requests.exceptions import RequestException -from typing import Any - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import ( - CONF_PASSWORD, - CONF_EMAIL, - CONF_NAME, - Platform -) -from homeassistant.exceptions import ConfigEntryNotReady + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.util import Throttle -from homeassistant.helpers.json import save_json -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util import Throttle -from .daikinskyport import DaikinSkyport, ExpiredTokenError from .const import ( _LOGGER, - DOMAIN, - MANUFACTURER, CONF_ACCESS_TOKEN, CONF_REFRESH_TOKEN, COORDINATOR, + DOMAIN, + MANUFACTURER, ) +from .daikinskyport import DaikinSkyport, ExpiredTokenError MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) UNDO_UPDATE_LISTENER = "undo_update_listener" @@ -41,6 +26,7 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER, Platform.CLIMATE, Platform.SWITCH] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DaikinSkyport as config entry.""" if hass.data.get(DOMAIN) is None: @@ -50,10 +36,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: email: str = entry.data[CONF_EMAIL] password: str = entry.data[CONF_PASSWORD] try: - name: str = entry.options[CONF_NAME] - except (NameError, KeyError): - name: str = entry.data[CONF_NAME] - try: access_token: str = entry.data[CONF_ACCESS_TOKEN] refresh_token: str = entry.data[CONF_REFRESH_TOKEN] except (NameError, KeyError): @@ -66,29 +48,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "ACCESS_TOKEN": access_token, "REFRESH_TOKEN": refresh_token, } - + assert entry.unique_id is not None unique_id = entry.unique_id _LOGGER.debug("Using email: %s", email) - - coordinator = DaikinSkyportData( - hass, config, unique_id, entry - ) + coordinator = DaikinSkyportData(hass, config, unique_id, entry) try: await coordinator._async_update_data() except ExpiredTokenError as ex: - _LOGGER.warn("Unable to refresh auth token.") + _LOGGER.warn(f"Unable to refresh auth token: {ex}") raise ConfigEntryNotReady("Unable to refresh token.") - + if coordinator.daikinskyport.thermostats is None: _LOGGER.error("No Daikin Skyport devices found to set up") return False - -# entry.async_on_unload(entry.add_update_listener(update_listener)) + # entry.async_on_unload(entry.add_update_listener(update_listener)) for platform in PLATFORMS: if entry.options.get(platform, True): @@ -98,18 +76,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: undo_listener + UNDO_UPDATE_LISTENER: undo_listener, } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" _LOGGER.debug("Unload Entry: %s", str(entry)) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok: @@ -117,7 +96,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return unload_ok @@ -127,9 +105,12 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await async_unload_entry(hass, entry) await async_setup_entry(hass, entry) + async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" _LOGGER.debug("Update listener: %s", str(entry)) + + # await hass.config_entries.async_reload(entry.entry_id) @@ -137,11 +118,8 @@ class DaikinSkyportData: """Get the latest data and update the states.""" def __init__( - self, - hass: HomeAssistant, - config, - unique_id: str, - entry: ConfigEntry) -> None: + self, hass: HomeAssistant, config, unique_id: str, entry: ConfigEntry + ) -> None: """Init the Daikin Skyport data object.""" self.platforms = [] try: @@ -156,13 +134,13 @@ def __init__( identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, name=self.name, - ) - + ) + @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _async_update_data(self): """Update data via library.""" try: - current = await self.hass.async_add_executor_job(self.daikinskyport.update) + await self.hass.async_add_executor_job(self.daikinskyport.update) _LOGGER.debug("Daikin Skyport _async_update_data") except ExpiredTokenError: _LOGGER.debug("Daikin Skyport tokens expired") @@ -188,4 +166,3 @@ async def async_refresh(self) -> bool: return True _LOGGER.error("Error refreshing Daikin Skyport tokens") return False - diff --git a/custom_components/daikinskyport/climate.py b/custom_components/daikinskyport/climate.py index c37f205..c3adebe 100644 --- a/custom_components/daikinskyport/climate.py +++ b/custom_components/daikinskyport/climate.py @@ -1,85 +1,78 @@ """Support for Daikin Skyport Thermostats.""" + import collections from datetime import datetime from typing import Optional +import homeassistant.helpers.config_validation as cv import voluptuous as vol - from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, - HVACAction ) from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - PRESET_AWAY, + ATTR_TARGET_TEMP_LOW, FAN_AUTO, - FAN_ON, + FAN_HIGH, FAN_LOW, FAN_MEDIUM, - FAN_HIGH, - PRESET_NONE, + FAN_ON, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, - PRECISION_HALVES, PRECISION_TENTHS, - STATE_OFF, STATE_ON, UnitOfTemperature, ) - -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DaikinSkyportData - from .const import ( _LOGGER, - DOMAIN, - DAIKIN_HVAC_MODE_OFF, - DAIKIN_HVAC_MODE_HEAT, - DAIKIN_HVAC_MODE_COOL, + COORDINATOR, DAIKIN_HVAC_MODE_AUTO, DAIKIN_HVAC_MODE_AUXHEAT, - COORDINATOR, + DAIKIN_HVAC_MODE_COOL, + DAIKIN_HVAC_MODE_HEAT, + DAIKIN_HVAC_MODE_OFF, + DOMAIN, ) -WEEKDAY = [ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +WEEKDAY = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -#Hold settings (manual mode) +# Hold settings (manual mode) HOLD_NEXT_TRANSITION = 0 HOLD_1HR = 60 HOLD_2HR = 120 HOLD_4HR = 240 HOLD_8HR = 480 -#Preset values +# Preset values PRESET_AWAY = "Away" PRESET_SCHEDULE = "Schedule" PRESET_MANUAL = "Manual" PRESET_TEMP_HOLD = "Temp Hold" FAN_SCHEDULE = "Schedule" -#Fan Schedule values +# Fan Schedule values ATTR_FAN_START_TIME = "start_time" ATTR_FAN_STOP_TIME = "end_time" ATTR_FAN_INTERVAL = "interval" ATTR_FAN_SPEED = "fan_speed" -#Night Mode values +# Night Mode values ATTR_NIGHT_MODE_START_TIME = "start_time" ATTR_NIGHT_MODE_END_TIME = "end_time" ATTR_NIGHT_MODE_ENABLE = "enable" -#Schedule Adjustment values +# Schedule Adjustment values ATTR_SCHEDULE_DAY = "day" ATTR_SCHEDULE_START_TIME = "start_time" ATTR_SCHEDULE_PART = "part" @@ -87,13 +80,13 @@ ATTR_SCHEDULE_PART_LABEL = "label" ATTR_SCHEDULE_HEATING_SETPOINT = "heat_temp_setpoint" ATTR_SCHEDULE_COOLING_SETPOINT = "cool_temp_setpoint" -ATTR_SCHEDULE_MODE = "mode" #Unknown what this does right now -ATTR_SCHEDULE_ACTION = "action" #Unknown what this does right now +ATTR_SCHEDULE_MODE = "mode" # Unknown what this does right now +ATTR_SCHEDULE_ACTION = "action" # Unknown what this does right now -#OneClean values +# OneClean values ATTR_ONECLEAN_ENABLED = "enable" -#Efficiency value +# Efficiency value ATTR_EFFICIENCY_ENABLED = "enable" # Order matters, because for reverse mapping we don't want to map HEAT to AUX @@ -151,7 +144,7 @@ HOLD_1HR: 60, HOLD_2HR: 120, HOLD_4HR: 240, - HOLD_8HR: 480 + HOLD_8HR: 480, } SERVICE_RESUME_PROGRAM = "daikin_resume_program" @@ -161,11 +154,7 @@ SERVICE_SET_ONECLEAN = "daikin_set_oneclean" SERVICE_PRIORITIZE_EFFICIENCY = "daikin_prioritize_efficiency" -RESUME_PROGRAM_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids - } -) +RESUME_PROGRAM_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) FAN_SCHEDULE_SCHEMA = vol.Schema( { @@ -173,7 +162,7 @@ vol.Optional(ATTR_FAN_START_TIME): cv.positive_int, vol.Optional(ATTR_FAN_STOP_TIME): cv.positive_int, vol.Optional(ATTR_FAN_INTERVAL): cv.positive_int, - vol.Optional(ATTR_FAN_SPEED): cv.positive_int + vol.Optional(ATTR_FAN_SPEED): cv.positive_int, } ) @@ -222,6 +211,7 @@ | ClimateEntityFeature.TURN_OFF ) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -234,7 +224,7 @@ async def async_setup_entry( for index in range(len(coordinator.daikinskyport.thermostats)): thermostat = coordinator.daikinskyport.get_thermostat(index) entities.append(Thermostat(coordinator, index, thermostat)) - + async_add_entities(entities, True) def resume_program_set_service(service: ServiceCall) -> None: @@ -242,7 +232,7 @@ def resume_program_set_service(service: ServiceCall) -> None: entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Resuming program for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: @@ -253,7 +243,7 @@ def resume_program_set_service(service: ServiceCall) -> None: def set_fan_schedule_service(service): """Set the fan schedule on the target thermostats.""" - + start = service.data.get(ATTR_FAN_START_TIME) stop = service.data.get(ATTR_FAN_STOP_TIME) interval = service.data.get(ATTR_FAN_INTERVAL) @@ -262,7 +252,7 @@ def set_fan_schedule_service(service): entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Setting fan schedule for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: @@ -273,7 +263,7 @@ def set_fan_schedule_service(service): def set_night_mode_service(service): """Set night mode on the target thermostats.""" - + start = service.data.get(ATTR_NIGHT_MODE_START_TIME) stop = service.data.get(ATTR_NIGHT_MODE_END_TIME) enable = service.data.get(ATTR_NIGHT_MODE_ENABLE) @@ -281,7 +271,7 @@ def set_night_mode_service(service): entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Setting night mode for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: @@ -303,11 +293,13 @@ def set_thermostat_schedule_service(service): entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Setting thermostat schedule for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: - thermostat.set_thermostat_schedule(day, start, part, enable, label, heating, cooling) + thermostat.set_thermostat_schedule( + day, start, part, enable, label, heating, cooling + ) _LOGGER.info("Thermostat schedule set for %s", entity) thermostat.schedule_update_ha_state(True) break @@ -319,7 +311,7 @@ def set_oneclean_service(service): entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Setting OneClean for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: @@ -335,7 +327,7 @@ def set_efficiency_service(service): entity_ids = service.data[ATTR_ENTITY_ID] _LOGGER.info("Setting efficiency for %s", entity_ids) - + for entity in entity_ids: for thermostat in entities: if thermostat.entity_id == entity: @@ -386,6 +378,7 @@ def set_efficiency_service(service): schema=EFFICIENCY_SCHEMA, ) + class Thermostat(ClimateEntity): """A thermostat class for Daikin Skyport Thermostats.""" @@ -407,7 +400,9 @@ def __init__(self, data, thermostat_index, thermostat): self._heat_setpoint = self.thermostat["hspActive"] self._hvac_mode = DAIKIN_HVAC_TO_HASS[self.thermostat["mode"]] if DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculate"]] == FAN_ON: - self._fan_mode = DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculateSpeed"] + 3] + self._fan_mode = DAIKIN_FAN_TO_HASS[ + self.thermostat["fanCirculateSpeed"] + 3 + ] else: self._fan_mode = DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculate"]] self._fan_speed = DAIKIN_FAN_SPEED_TO_HASS[self.thermostat["fanCirculateSpeed"]] @@ -423,19 +418,32 @@ def __init__(self, data, thermostat_index, thermostat): self._operation_list = [] if self.thermostat["ctSystemCapHeat"]: self._operation_list.append(HVACMode.HEAT) - if (("ctOutdoorNoofCoolStages" in self.thermostat and self.thermostat["ctOutdoorNoofCoolStages"] > 0) - or ("P1P2S21CoolingCapability" in self.thermostat and self.thermostat["P1P2S21CoolingCapability"] == True)): + if ( + "ctOutdoorNoofCoolStages" in self.thermostat + and self.thermostat["ctOutdoorNoofCoolStages"] > 0 + ) or ( + "P1P2S21CoolingCapability" in self.thermostat + and self.thermostat["P1P2S21CoolingCapability"] is True + ): self._operation_list.append(HVACMode.COOL) if len(self._operation_list) == 2: self._operation_list.insert(0, HVACMode.AUTO) self._operation_list.append(HVACMode.OFF) - self._preset_modes = {PRESET_SCHEDULE, - PRESET_MANUAL, - PRESET_TEMP_HOLD, - PRESET_AWAY - } - self._fan_modes = [FAN_AUTO, FAN_ON, FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_SCHEDULE] + self._preset_modes = { + PRESET_SCHEDULE, + PRESET_MANUAL, + PRESET_TEMP_HOLD, + PRESET_AWAY, + } + self._fan_modes = [ + FAN_AUTO, + FAN_ON, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_SCHEDULE, + ] self.update_without_throttle = False async def async_update(self): @@ -451,7 +459,9 @@ async def async_update(self): self._heat_setpoint = self.thermostat["hspActive"] self._hvac_mode = DAIKIN_HVAC_TO_HASS[self.thermostat["mode"]] if DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculate"]] == FAN_ON: - self._fan_mode = DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculateSpeed"] + 3] + self._fan_mode = DAIKIN_FAN_TO_HASS[ + self.thermostat["fanCirculateSpeed"] + 3 + ] else: self._fan_mode = DAIKIN_FAN_TO_HASS[self.thermostat["fanCirculate"]] self._fan_speed = DAIKIN_FAN_SPEED_TO_HASS[self.thermostat["fanCirculateSpeed"]] @@ -471,7 +481,7 @@ def device_info(self) -> DeviceInfo: @property def available(self): """Return if device is available.""" - return True #TBD: Need to determine how to tell if the thermostat is available or not + return True # TBD: Need to determine how to tell if the thermostat is available or not @property def supported_features(self): @@ -516,7 +526,10 @@ def target_temperature(self): @property def fan(self): """Return the current fan status.""" - if "ctAHFanCurrentDemandStatus" in self.thermostat and self.thermostat["ctAHFanCurrentDemandStatus"] > 0: + if ( + "ctAHFanCurrentDemandStatus" in self.thermostat + and self.thermostat["ctAHFanCurrentDemandStatus"] > 0 + ): return STATE_ON return HVACMode.OFF @@ -563,7 +576,6 @@ def hvac_action(self): @property def extra_state_attributes(self): """Return device specific state attributes.""" - status = self.thermostat["equipmentStatus"] fan_cfm = "Unavailable" fan_demand = "Unavailable" cooling_demand = "Unavailable" @@ -573,34 +585,45 @@ def extra_state_attributes(self): humidification_demand = "Unavailable" indoor_mode = "Unavailable" - if "ctAHCurrentIndoorAirflow" in self.thermostat: + if "ctAHCurrentIndoorAirflow" in self.thermostat: if self.thermostat["ctAHCurrentIndoorAirflow"] == 65535: fan_cfm = self.thermostat["ctIFCIndoorBlowerAirflow"] else: fan_cfm = self.thermostat["ctAHCurrentIndoorAirflow"] - + if "ctAHFanCurrentDemandStatus" in self.thermostat: fan_demand = round(self.thermostat["ctAHFanCurrentDemandStatus"] / 2, 1) - + if "ctOutdoorCoolRequestedDemand" in self.thermostat: - cooling_demand = round(self.thermostat["ctOutdoorCoolRequestedDemand"] / 2, 1) + cooling_demand = round( + self.thermostat["ctOutdoorCoolRequestedDemand"] / 2, 1 + ) if "ctAHHeatRequestedDemand" in self.thermostat: heating_demand = round(self.thermostat["ctAHHeatRequestedDemand"] / 2, 1) if "ctOutdoorHeatRequestedDemand" in self.thermostat: - heatpump_demand = round(self.thermostat["ctOutdoorHeatRequestedDemand"] / 2, 1) + heatpump_demand = round( + self.thermostat["ctOutdoorHeatRequestedDemand"] / 2, 1 + ) if "ctOutdoorDeHumidificationRequestedDemand" in self.thermostat: - dehumidification_demand = round(self.thermostat["ctOutdoorDeHumidificationRequestedDemand"] / 2, 1) + dehumidification_demand = round( + self.thermostat["ctOutdoorDeHumidificationRequestedDemand"] / 2, 1 + ) if "ctAHHumidificationRequestedDemand" in self.thermostat: - humidification_demand = round(self.thermostat["ctAHHumidificationRequestedDemand"] / 2, 1) + humidification_demand = round( + self.thermostat["ctAHHumidificationRequestedDemand"] / 2, 1 + ) if "ctAHUnitType" in self.thermostat and self.thermostat["ctAHUnitType"] != 255: - indoor_mode=self.thermostat["ctAHMode"].strip() - elif "ctIFCUnitType" in self.thermostat and self.thermostat["ctIFCUnitType"] != 255: - indoor_mode=self.thermostat["ctIFCOperatingHeatCoolMode"].strip() + indoor_mode = self.thermostat["ctAHMode"].strip() + elif ( + "ctIFCUnitType" in self.thermostat + and self.thermostat["ctIFCUnitType"] != 255 + ): + indoor_mode = self.thermostat["ctIFCOperatingHeatCoolMode"].strip() outdoor_mode = "Unavailable" if "ctOutdoorMode" in self.thermostat: @@ -622,10 +645,9 @@ def extra_state_attributes(self): "indoor_mode": indoor_mode, "outdoor_mode": outdoor_mode, "thermostat_unlocked": bool(self.thermostat["displayLockPIN"] == 0), - "media_filter_days": self.thermostat["alertMediaAirFilterDays"] + "media_filter_days": self.thermostat["alertMediaAirFilterDays"], } - def set_preset_mode(self, preset_mode): """Activate a preset.""" if preset_mode == self.preset_mode: @@ -641,13 +663,13 @@ def set_preset_mode(self, preset_mode): elif preset_mode == PRESET_MANUAL: self.data.daikinskyport.set_away(self.thermostat_index, False) self.data.daikinskyport.set_permanent_hold(self.thermostat_index) - + elif preset_mode == PRESET_TEMP_HOLD: self.data.daikinskyport.set_away(self.thermostat_index, False) self.data.daikinskyport.set_temp_hold(self.thermostat_index) else: return - + self._preset_mode = preset_mode self.update_without_throttle = True @@ -671,21 +693,19 @@ def set_auto_temp_hold(self, heat_temp, cool_temp): if self._preset_mode == PRESET_MANUAL: self.data.daikinskyport.set_permanent_hold( - self.thermostat_index, - cool_temp_setpoint, - heat_temp_setpoint - ) + self.thermostat_index, cool_temp_setpoint, heat_temp_setpoint + ) else: self.data.daikinskyport.set_temp_hold( self.thermostat_index, cool_temp_setpoint, heat_temp_setpoint, self.hold_preference(), - ) - + ) + self._cool_setpoint = cool_temp_setpoint self._heat_setpoint = heat_temp_setpoint - + _LOGGER.debug( "Setting Daikin Skyport hold_temp to: heat=%s, is=%s, " "cool=%s, is=%s", heat_temp, @@ -700,41 +720,39 @@ def set_fan_mode(self, fan_mode): """Set the fan mode. Valid values are "on", "auto", or "schedule".""" if fan_mode in {FAN_ON, FAN_AUTO, FAN_SCHEDULE}: self.data.daikinskyport.set_fan_mode( - self.thermostat_index, - FAN_TO_DAIKIN_FAN[fan_mode] + self.thermostat_index, FAN_TO_DAIKIN_FAN[fan_mode] ) - + self._fan_mode = fan_mode self.update_without_throttle = True _LOGGER.debug("Setting fan mode to: %s", fan_mode) elif fan_mode in {FAN_LOW, FAN_MEDIUM, FAN_HIGH}: - # Start the fan if it's off. + # Start the fan if it's off. if self._fan_mode == FAN_AUTO: self.data.daikinskyport.set_fan_mode( - self.thermostat_index, - FAN_TO_DAIKIN_FAN[FAN_ON] + self.thermostat_index, FAN_TO_DAIKIN_FAN[FAN_ON] ) - + self._fan_mode = fan_mode _LOGGER.debug("Setting fan mode to: %s", fan_mode) self.data.daikinskyport.set_fan_speed( - self.thermostat_index, - FAN_TO_DAIKIN_FAN[fan_mode] + self.thermostat_index, FAN_TO_DAIKIN_FAN[fan_mode] ) - + self._fan_speed = FAN_TO_DAIKIN_FAN[fan_mode] self.update_without_throttle = True _LOGGER.debug("Setting fan speed to: %s", self._fan_speed) else: - error = "Invalid fan_mode value: Valid values are 'on', 'auto', or 'schedule'" + error = ( + "Invalid fan_mode value: Valid values are 'on', 'auto', or 'schedule'" + ) _LOGGER.error(error) return - def set_temp_hold(self, temp): """Set temperature hold in modes other than auto.""" if self.hvac_mode == HVACMode.HEAT: @@ -766,7 +784,6 @@ def set_temperature(self, **kwargs): self._cool_setpoint = high_temp self._heat_setpoint = low_temp - def set_humidity(self, humidity): """Set the humidity level.""" self.data.daikinskyport.set_humidity(self.thermostat_index, humidity) @@ -785,9 +802,7 @@ def set_hvac_mode(self, hvac_mode): def resume_program(self): """Resume the thermostat schedule program.""" - self.data.daikinskyport.resume_program( - self.thermostat_index - ) + self.data.daikinskyport.resume_program(self.thermostat_index) self.update_without_throttle = True def set_fan_schedule(self, start=None, stop=None, interval=None, speed=None): @@ -816,7 +831,16 @@ def set_night_mode(self, start=None, stop=None, enable=None): ) self.update_without_throttle = True - def set_thermostat_schedule(self, day=None, start=None, part=None, enable=None, label=None, heating=None, cooling=None): + def set_thermostat_schedule( + self, + day=None, + start=None, + part=None, + enable=None, + label=None, + heating=None, + cooling=None, + ): """Set the thermostat schedule.""" if day is None: now = datetime.now() @@ -846,16 +870,12 @@ def set_thermostat_schedule(self, day=None, start=None, part=None, enable=None, def set_oneclean(self, enable): """Enable/disable OneClean.""" - self.data.daikinskyport.set_fan_clean( - self.thermostat_index, enable - ) + self.data.daikinskyport.set_fan_clean(self.thermostat_index, enable) self.update_without_throttle = True def set_efficiency(self, enable): """Enable/disable heat pump efficiency.""" - self.data.daikinskyport.set_dual_fuel_efficiency( - self.thermostat_index, enable - ) + self.data.daikinskyport.set_dual_fuel_efficiency(self.thermostat_index, enable) self.update_without_throttle = True def hold_preference(self): diff --git a/custom_components/daikinskyport/config_flow.py b/custom_components/daikinskyport/config_flow.py index c5a0ff3..df51998 100644 --- a/custom_components/daikinskyport/config_flow.py +++ b/custom_components/daikinskyport/config_flow.py @@ -1,25 +1,17 @@ from __future__ import annotations -import asyncio -from typing import Any -from requests.exceptions import RequestException -from async_timeout import timeout -from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_NAME +import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, SchemaOptionsFlowHandler, ) -from .const import ( - DOMAIN, - CONF_ACCESS_TOKEN, - CONF_REFRESH_TOKEN, -) -import voluptuous as vol + +from .const import CONF_ACCESS_TOKEN, CONF_REFRESH_TOKEN, DOMAIN from .daikinskyport import DaikinSkyport OPTIONS_SCHEMA = vol.Schema( @@ -31,21 +23,28 @@ "init": SchemaFlowFormStep(OPTIONS_SCHEMA), } + class DaikinSkyportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # The schema version of the entries that it creates # Home Assistant will call your migrate method if the version changes VERSION = 1 - + async def async_step_user(self, user_input=None): self._abort_if_unique_id_configured() if user_input is not None: - daikinskyport = DaikinSkyport(config={ - 'EMAIL': user_input[CONF_EMAIL], - 'PASSWORD': user_input[CONF_PASSWORD], - }) - result = await self.hass.async_add_executor_job(daikinskyport.request_tokens) + daikinskyport = DaikinSkyport( + config={ + "EMAIL": user_input[CONF_EMAIL], + "PASSWORD": user_input[CONF_PASSWORD], + } + ) + result = await self.hass.async_add_executor_job( + daikinskyport.request_tokens + ) if result is None: - raise HomeAssistantError("Authentication failure. Verify username and password are correct.") + raise HomeAssistantError( + "Authentication failure. Verify username and password are correct." + ) await self.async_set_unique_id( daikinskyport.user_email, raise_on_progress=False @@ -54,21 +53,19 @@ async def async_step_user(self, user_input=None): user_input[CONF_ACCESS_TOKEN] = daikinskyport.access_token user_input[CONF_REFRESH_TOKEN] = daikinskyport.refresh_token - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + return self.async_show_form( - step_id="user", + step_id="user", data_schema=vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_NAME, default="Daikin"): str, - } - ) + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_NAME, default="Daikin"): str, + } + ), ) - + @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: diff --git a/custom_components/daikinskyport/const.py b/custom_components/daikinskyport/const.py index 4dad2f1..351d25a 100644 --- a/custom_components/daikinskyport/const.py +++ b/custom_components/daikinskyport/const.py @@ -1,51 +1,43 @@ import logging -_LOGGER = logging.getLogger(__package__) - -DOMAIN = "daikinskyport" -MANUFACTURER = "Daikin" - -# Full list of HA conditions as of 10/2023 from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_EXCEPTIONAL, ATTR_CONDITION_FOG, - ATTR_CONDITION_HAIL, ATTR_CONDITION_LIGHTNING, - ATTR_CONDITION_LIGHTNING_RAINY, ATTR_CONDITION_PARTLYCLOUDY, - ATTR_CONDITION_POURING, ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, - ATTR_CONDITION_WINDY, - ATTR_CONDITION_WINDY_VARIANT, ) +DOMAIN = "daikinskyport" +MANUFACTURER = "Daikin" + +_LOGGER = logging.getLogger(__package__) + # Map Daikin weather icons to HA conditions (weather icons are always the same, *Cond change with language) # Unknown entries are unverifed. Taken from Weather Underground icon names DAIKIN_WEATHER_ICON_TO_HASS = { - "sunny": ATTR_CONDITION_SUNNY, #Unknown - "mostlysunny": ATTR_CONDITION_SUNNY, #Unknown - "partlysunny": ATTR_CONDITION_PARTLYCLOUDY, #Unknown + "sunny": ATTR_CONDITION_SUNNY, # Unknown + "mostlysunny": ATTR_CONDITION_SUNNY, # Unknown + "partlysunny": ATTR_CONDITION_PARTLYCLOUDY, # Unknown "partlycloudy": ATTR_CONDITION_PARTLYCLOUDY, - "clear": ATTR_CONDITION_CLEAR_NIGHT, #Unknown + "clear": ATTR_CONDITION_CLEAR_NIGHT, # Unknown "mostlycloudy": ATTR_CONDITION_CLOUDY, - "cloudy": ATTR_CONDITION_CLOUDY, #Unknown + "cloudy": ATTR_CONDITION_CLOUDY, # Unknown "rain": ATTR_CONDITION_RAINY, "chancerain": ATTR_CONDITION_RAINY, - "snow": ATTR_CONDITION_SNOWY, #Unknown - "chancesnow": ATTR_CONDITION_SNOWY, #Unknown - "chanceflurries": ATTR_CONDITION_SNOWY, #Unknown - "flurries": ATTR_CONDITION_SNOWY, #Unknown + "snow": ATTR_CONDITION_SNOWY, # Unknown + "chancesnow": ATTR_CONDITION_SNOWY, # Unknown + "chanceflurries": ATTR_CONDITION_SNOWY, # Unknown + "flurries": ATTR_CONDITION_SNOWY, # Unknown "tstorms": ATTR_CONDITION_LIGHTNING, "chancetstorms": ATTR_CONDITION_LIGHTNING, - "fog": ATTR_CONDITION_FOG, #Unknown - "hazy": "hazy", #Unknown - "sleet": "sleet", #Unknown - "chancesleet": "sleet", #Unknown + "fog": ATTR_CONDITION_FOG, # Unknown + "hazy": "hazy", # Unknown + "sleet": "sleet", # Unknown + "chancesleet": "sleet", # Unknown } # The multiplier applied by the API to percentage values. diff --git a/custom_components/daikinskyport/daikinskyport.py b/custom_components/daikinskyport/daikinskyport.py index 1a2d677..adc8bbb 100644 --- a/custom_components/daikinskyport/daikinskyport.py +++ b/custom_components/daikinskyport/daikinskyport.py @@ -1,31 +1,33 @@ -''' Python Code for Communication with the Daikin Skyport Thermostat. This is taken mostly from pyecobee, so much credit to those contributors''' -import requests +"""Python Code for Communication with the Daikin Skyport Thermostat. This is taken mostly from pyecobee, so much credit to those contributors""" + import json -import os import logging -from time import sleep +import os -from requests.exceptions import RequestException +import requests from requests.adapters import HTTPAdapter +from requests.exceptions import RequestException from requests.packages.urllib3.util.retry import Retry from .const import DAIKIN_PERCENT_MULTIPLIER -logger = logging.getLogger('daikinskyport') +logger = logging.getLogger("daikinskyport") NEXT_SCHEDULE = 1 + class ExpiredTokenError(Exception): """Raised when Daikin Skyport API returns a code indicating expired credentials.""" pass + def config_from_file(filename, config=None): - ''' Small configuration file management function''' + """Small configuration file management function""" if config: # We're writing configuration try: - with open(filename, 'w') as fdesc: + with open(filename, "w") as fdesc: fdesc.write(json.dumps(config)) except IOError as error: logger.exception(error) @@ -35,18 +37,20 @@ def config_from_file(filename, config=None): # We're reading config if os.path.isfile(filename): try: - with open(filename, 'r') as fdesc: + with open(filename, "r") as fdesc: return json.loads(fdesc.read()) - except IOError as error: + except IOError: return False else: return {} class DaikinSkyport(object): - ''' Class for storing Daikin Skyport Thermostats and Sensors ''' + """Class for storing Daikin Skyport Thermostats and Sensors""" - def __init__(self, config_filename=None, user_email=None, user_password=None, config=None): + def __init__( + self, config_filename=None, user_email=None, user_password=None, config=None + ): self.thermostats = list() self.thermostatlist = list() self.authenticated = False @@ -59,48 +63,51 @@ def __init__(self, config_filename=None, user_email=None, user_password=None, co logger.error("Error. No user email or password was supplied.") return jsonconfig = {"EMAIL": user_email, "PASSWORD": user_password} - config_filename = 'daikinskyport.conf' + config_filename = "daikinskyport.conf" config_from_file(config_filename, jsonconfig) config = config_from_file(config_filename) else: self.file_based_config = False - if 'EMAIL' in config: - self.user_email = config['EMAIL'] + if "EMAIL" in config: + self.user_email = config["EMAIL"] else: logger.error("Email missing from config.") - if 'PASSWORD' in config: # PASSWORD is only needed during first login - self.user_password = config['PASSWORD'] + if "PASSWORD" in config: # PASSWORD is only needed during first login + self.user_password = config["PASSWORD"] - if 'ACCESS_TOKEN' in config: - self.access_token = config['ACCESS_TOKEN'] + if "ACCESS_TOKEN" in config: + self.access_token = config["ACCESS_TOKEN"] else: - self.access_token = '' + self.access_token = "" - if 'REFRESH_TOKEN' in config: - self.refresh_token = config['REFRESH_TOKEN'] + if "REFRESH_TOKEN" in config: + self.refresh_token = config["REFRESH_TOKEN"] else: - self.refresh_token = '' -# self.request_tokens() -# return + self.refresh_token = "" + + # self.request_tokens() + # return -# self.update() + # self.update() def request_tokens(self): - ''' Method to request API tokens from skyport ''' - url = 'https://api.daikinskyport.com/users/auth/login' - header = {'Accept': 'application/json', - 'Content-Type': 'application/json'} + """Method to request API tokens from skyport""" + url = "https://api.daikinskyport.com/users/auth/login" + header = {"Accept": "application/json", "Content-Type": "application/json"} data = {"email": self.user_email, "password": self.user_password} try: request = requests.post(url, headers=header, json=data) except RequestException as e: - logger.error("Error connecting to Daikin Skyport. Possible connectivity outage." - "Could not request token. %s", e) + logger.error( + "Error connecting to Daikin Skyport. Possible connectivity outage." + "Could not request token. %s", + e, + ) return False if request.status_code == requests.codes.ok: json_data = request.json() - self.access_token = json_data['accessToken'] - self.refresh_token = json_data['refreshToken'] + self.access_token = json_data["accessToken"] + self.refresh_token = json_data["refreshToken"] if self.refresh_token is None: logger.error("Auth did not return a refresh token.") else: @@ -108,36 +115,44 @@ def request_tokens(self): self.write_tokens_to_file() return json_data else: - logger.error('Error while requesting tokens from daikinskyport.com.' - ' Status code: %s Message: %s', request.status_code, request.text) + logger.error( + "Error while requesting tokens from daikinskyport.com." + " Status code: %s Message: %s", + request.status_code, + request.text, + ) return False def refresh_tokens(self): - ''' Method to refresh API tokens from daikinskyport.com ''' - url = 'https://api.daikinskyport.com/users/auth/token' - header = {'Accept': 'application/json', - 'Content-Type': 'application/json'} - data = {'email': self.user_email, - 'refreshToken': self.refresh_token} + """Method to refresh API tokens from daikinskyport.com""" + url = "https://api.daikinskyport.com/users/auth/token" + header = {"Accept": "application/json", "Content-Type": "application/json"} + data = {"email": self.user_email, "refreshToken": self.refresh_token} request = requests.post(url, headers=header, json=data) if request.status_code == requests.codes.ok: json_data = request.json() - self.access_token = json_data['accessToken'] + self.access_token = json_data["accessToken"] if self.file_based_config: self.write_tokens_to_file() return True else: - logger.warn("Could not refresh tokens, Trying to re-request. Status code: %s Message: %s ", request.status_code, request.text) + logger.warn( + "Could not refresh tokens, Trying to re-request. Status code: %s Message: %s ", + request.status_code, + request.text, + ) result = self.request_tokens() if result is not None: return True return False def get_thermostats(self): - ''' Set self.thermostats to a json list of thermostats from daikinskyport.com ''' - url = 'https://api.daikinskyport.com/devices' - header = {'Content-Type': 'application/json;charset=UTF-8', - 'Authorization': 'Bearer ' + self.access_token} + """Set self.thermostats to a json list of thermostats from daikinskyport.com""" + url = "https://api.daikinskyport.com/devices" + header = { + "Content-Type": "application/json;charset=UTF-8", + "Authorization": "Bearer " + self.access_token, + } retry_strategy = Retry(total=8, backoff_factor=0.1) adapter = HTTPAdapter(max_retries=retry_strategy) http = requests.Session() @@ -147,21 +162,24 @@ def get_thermostats(self): try: request = http.get(url, headers=header) except RequestException as e: - logger.warn("Error connecting to Daikin Skyport. Possible connectivity outage: %s", e) + logger.warn( + "Error connecting to Daikin Skyport. Possible connectivity outage: %s", + e, + ) return None if request.status_code == requests.codes.ok: self.authenticated = True self.thermostatlist = request.json() for thermostat in self.thermostatlist: overwrite = False - thermostat_info = self.get_thermostat_info(thermostat['id']) - if thermostat_info == None: + thermostat_info = self.get_thermostat_info(thermostat["id"]) + if thermostat_info is None: continue - thermostat_info['name'] = thermostat['name'] - thermostat_info['id'] = thermostat['id'] - thermostat_info['model'] = thermostat['model'] + thermostat_info["name"] = thermostat["name"] + thermostat_info["id"] = thermostat["id"] + thermostat_info["model"] = thermostat["model"] for index in range(len(self.thermostats)): - if thermostat['id'] == self.thermostats[index]['id']: + if thermostat["id"] == self.thermostats[index]["id"]: overwrite = True self.thermostats[index] = thermostat_info if not overwrite: @@ -169,16 +187,21 @@ def get_thermostats(self): return self.thermostats else: self.authenticated = False - logger.debug("Error connecting to Daikin Skyport while attempting to get " - "thermostat data. Status code: %s Message: %s", request.status_code, request.text) - raise ExpiredTokenError ("Daikin Skyport token expired") - return None + logger.debug( + "Error connecting to Daikin Skyport while attempting to get " + "thermostat data. Status code: %s Message: %s", + request.status_code, + request.text, + ) + raise ExpiredTokenError("Daikin Skyport token expired") def get_thermostat_info(self, deviceid): - ''' Retrieve the device info for the specific device ''' - url = 'https://api.daikinskyport.com/deviceData/' + deviceid - header = {'Content-Type': 'application/json;charset=UTF-8', - 'Authorization': 'Bearer ' + self.access_token} + """Retrieve the device info for the specific device""" + url = "https://api.daikinskyport.com/deviceData/" + deviceid + header = { + "Content-Type": "application/json;charset=UTF-8", + "Authorization": "Bearer " + self.access_token, + } retry_strategy = Retry(total=8, backoff_factor=0.1) adapter = HTTPAdapter(max_retries=retry_strategy) http = requests.Session() @@ -189,89 +212,309 @@ def get_thermostat_info(self, deviceid): request = http.get(url, headers=header) request.raise_for_status() except requests.exceptions.HTTPError as e: - if e.response.status_code == 400 and e.response.json().get("message") == "DeviceOfflineException": + if ( + e.response.status_code == 400 + and e.response.json().get("message") == "DeviceOfflineException" + ): logger.warn("Device is offline: %s", deviceid) self.authenticated = True return None else: self.authenticated = False - logger.debug("Error connecting to Daikin Skyport while attempting to get " - "thermostat data. Status code: %s Message: %s", request.status_code, request.text) - raise ExpiredTokenError ("Daikin Skyport token expired") - return None + logger.debug( + "Error connecting to Daikin Skyport while attempting to get " + "thermostat data. Status code: %s Message: %s", + request.status_code, + request.text, + ) + raise ExpiredTokenError("Daikin Skyport token expired") if request.status_code == requests.codes.ok: self.authenticated = True return request.json() else: self.authenticated = False - logger.debug("Error connecting to Daikin Skyport while attempting to get " - "thermostat data. Status code: %s Message: %s", request.status_code, request.text) - raise ExpiredTokenError ("Daikin Skyport token expired") - return None + logger.debug( + "Error connecting to Daikin Skyport while attempting to get " + "thermostat data. Status code: %s Message: %s", + request.status_code, + request.text, + ) + raise ExpiredTokenError("Daikin Skyport token expired") def get_thermostat(self, index): - ''' Return a single thermostat based on index ''' + """Return a single thermostat based on index""" return self.thermostats[index] def get_sensors(self, index): - ''' Return sensors based on index ''' + """Return sensors based on index""" sensors = list() thermostat = self.thermostats[index] - name = thermostat['name'] - sensors.append({"name": f"{name} Outdoor", "value": thermostat['tempOutdoor'], "type": "temperature"}) - sensors.append({"name": f"{name} Outdoor", "value": thermostat['humOutdoor'], "type": "humidity"}) + name = thermostat["name"] + sensors.append( + { + "name": f"{name} Outdoor", + "value": thermostat["tempOutdoor"], + "type": "temperature", + } + ) + sensors.append( + { + "name": f"{name} Outdoor", + "value": thermostat["humOutdoor"], + "type": "humidity", + } + ) if "ctOutdoorFanRequestedDemandPercentage" in thermostat: - sensors.append({"name": f"{name} Outdoor fan", "value": round(thermostat['ctOutdoorFanRequestedDemandPercentage'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Outdoor fan", + "value": round( + thermostat["ctOutdoorFanRequestedDemandPercentage"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctOutdoorHeatRequestedDemand" in thermostat: - sensors.append({"name": f"{name} Outdoor heat pump", "value": round(thermostat['ctOutdoorHeatRequestedDemand'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Outdoor heat pump", + "value": round( + thermostat["ctOutdoorHeatRequestedDemand"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctOutdoorCoolRequestedDemand" in thermostat: - sensors.append({"name": f"{name} Outdoor cooling", "value": round(thermostat['ctOutdoorCoolRequestedDemand'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Outdoor cooling", + "value": round( + thermostat["ctOutdoorCoolRequestedDemand"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctOutdoorPower" in thermostat: - sensors.append({"name": f"{name} Outdoor", "value": thermostat['ctOutdoorPower'] * 10, "type": "power"}) + sensors.append( + { + "name": f"{name} Outdoor", + "value": thermostat["ctOutdoorPower"] * 10, + "type": "power", + } + ) if "ctOutdoorFrequencyInPercent" in thermostat: - sensors.append({"name": f"{name} Outdoor", "value": round(thermostat['ctOutdoorFrequencyInPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "frequency_percent"}) + sensors.append( + { + "name": f"{name} Outdoor", + "value": round( + thermostat["ctOutdoorFrequencyInPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "frequency_percent", + } + ) if "tempIndoor" in thermostat: - sensors.append({"name": f"{name} Indoor", "value": thermostat['tempIndoor'], "type": "temperature"}) + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["tempIndoor"], + "type": "temperature", + } + ) if "humIndoor" in thermostat: - sensors.append({"name": f"{name} Indoor", "value": thermostat['humIndoor'], "type": "humidity"}) + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["humIndoor"], + "type": "humidity", + } + ) if "ctIFCFanRequestedDemandPercent" in thermostat: - sensors.append({"name": f"{name} Indoor fan", "value": round(thermostat['ctIFCFanRequestedDemandPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Indoor fan", + "value": round( + thermostat["ctIFCFanRequestedDemandPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctIFCCurrentFanActualStatus" in thermostat: - sensors.append({"name": f"{name} Indoor fan", "value": round(thermostat['ctIFCCurrentFanActualStatus'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "actual_status"}) + sensors.append( + { + "name": f"{name} Indoor fan", + "value": round( + thermostat["ctIFCCurrentFanActualStatus"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "actual_status", + } + ) if "ctIFCCoolRequestedDemandPercent" in thermostat: - sensors.append({"name": f"{name} Indoor cooling", "value": round(thermostat['ctIFCCoolRequestedDemandPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Indoor cooling", + "value": round( + thermostat["ctIFCCoolRequestedDemandPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctIFCCurrentCoolActualStatus" in thermostat: - sensors.append({"name": f"{name} Indoor cooling", "value": round(thermostat['ctIFCCurrentCoolActualStatus'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "actual_status"}) + sensors.append( + { + "name": f"{name} Indoor cooling", + "value": round( + thermostat["ctIFCCurrentCoolActualStatus"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "actual_status", + } + ) if "ctIFCHeatRequestedDemandPercent" in thermostat: - sensors.append({"name": f"{name} Indoor furnace", "value": round(thermostat['ctIFCHeatRequestedDemandPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Indoor furnace", + "value": round( + thermostat["ctIFCHeatRequestedDemandPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctIFCCurrentHeatActualStatus" in thermostat: - sensors.append({"name": f"{name} Indoor furnace", "value": round(thermostat['ctIFCCurrentHeatActualStatus'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "actual_status"}) + sensors.append( + { + "name": f"{name} Indoor furnace", + "value": round( + thermostat["ctIFCCurrentHeatActualStatus"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "actual_status", + } + ) if "ctIFCHumRequestedDemandPercent" in thermostat: - sensors.append({"name": f"{name} Indoor humidifier", "value": round(thermostat['ctIFCHumRequestedDemandPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Indoor humidifier", + "value": round( + thermostat["ctIFCHumRequestedDemandPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctIFCDehumRequestedDemandPercent" in thermostat: - sensors.append({"name": f"{name} Indoor dehumidifier", "value": round(thermostat['ctIFCDehumRequestedDemandPercent'] / DAIKIN_PERCENT_MULTIPLIER, 1), "type": "demand"}) + sensors.append( + { + "name": f"{name} Indoor dehumidifier", + "value": round( + thermostat["ctIFCDehumRequestedDemandPercent"] + / DAIKIN_PERCENT_MULTIPLIER, + 1, + ), + "type": "demand", + } + ) if "ctOutdoorAirTemperature" in thermostat: - sensors.append({"name": f"{name} Outdoor air", "value": round(((thermostat['ctOutdoorAirTemperature'] / 10) - 32) * 5 / 9, 1), "type": "temperature"}) + sensors.append( + { + "name": f"{name} Outdoor air", + "value": round( + ((thermostat["ctOutdoorAirTemperature"] / 10) - 32) * 5 / 9, 1 + ), + "type": "temperature", + } + ) if "ctIFCIndoorBlowerAirflow" in thermostat: - sensors.append({"name": f"{name} Indoor furnace blower", "value": thermostat['ctIFCIndoorBlowerAirflow'], "type": "airflow"}) + sensors.append( + { + "name": f"{name} Indoor furnace blower", + "value": thermostat["ctIFCIndoorBlowerAirflow"], + "type": "airflow", + } + ) if "ctAHCurrentIndoorAirflow" in thermostat: - sensors.append({"name": f"{name} Indoor air handler blower", "value": thermostat['ctAHCurrentIndoorAirflow'], "type": "airflow"}) + sensors.append( + { + "name": f"{name} Indoor air handler blower", + "value": thermostat["ctAHCurrentIndoorAirflow"], + "type": "airflow", + } + ) - ''' if equipment is idle, set power to zero rather than accept bogus data ''' - if thermostat['equipmentStatus'] == 5: + """ if equipment is idle, set power to zero rather than accept bogus data """ + if thermostat["equipmentStatus"] == 5: sensors.append({"name": f"{name} Indoor", "value": 0, "type": "power"}) elif "ctIndoorPower" in thermostat: - sensors.append({"name": f"{name} Indoor", "value": thermostat['ctIndoorPower'], "type": "power"}) - - - if self.thermostats[index]['aqOutdoorAvailable']: - sensors.append({"name": f"{name} Outdoor", "value": thermostat['aqOutdoorParticles'], "type": "particle"}) - sensors.append({"name": f"{name} Outdoor", "value": thermostat['aqOutdoorValue'], "type": "score"}) - sensors.append({"name": f"{name} Outdoor", "value": round(thermostat['aqOutdoorOzone'] * 1.96), "type": "ozone"}) - if self.thermostats[index]['aqIndoorAvailable']: - sensors.append({"name": f"{name} Indoor", "value": thermostat['aqIndoorParticlesValue'], "type": "particle"}) - sensors.append({"name": f"{name} Indoor", "value": thermostat['aqIndoorValue'], "type": "score"}) - sensors.append({"name": f"{name} Indoor", "value": thermostat['aqIndoorVOCValue'], "type": "VOC"}) + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["ctIndoorPower"], + "type": "power", + } + ) + + if self.thermostats[index]["aqOutdoorAvailable"]: + sensors.append( + { + "name": f"{name} Outdoor", + "value": thermostat["aqOutdoorParticles"], + "type": "particle", + } + ) + sensors.append( + { + "name": f"{name} Outdoor", + "value": thermostat["aqOutdoorValue"], + "type": "score", + } + ) + sensors.append( + { + "name": f"{name} Outdoor", + "value": round(thermostat["aqOutdoorOzone"] * 1.96), + "type": "ozone", + } + ) + if self.thermostats[index]["aqIndoorAvailable"]: + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["aqIndoorParticlesValue"], + "type": "particle", + } + ) + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["aqIndoorValue"], + "type": "score", + } + ) + sensors.append( + { + "name": f"{name} Indoor", + "value": thermostat["aqIndoorVOCValue"], + "type": "VOC", + } + ) fault_sensors = [ ("ctAHCriticalFault", "Air Handler Critical Fault"), @@ -288,23 +531,29 @@ def get_sensors(self, index): for fault_key, fault_name in fault_sensors: if fault_key in thermostat: - sensors.append({"name": f"{name} {fault_name}", "value": thermostat[fault_key], "type": "fault_code"}) + sensors.append( + { + "name": f"{name} {fault_name}", + "value": thermostat[fault_key], + "type": "fault_code", + } + ) return sensors def write_tokens_to_file(self): - ''' Write api tokens to a file ''' + """Write api tokens to a file""" config = dict() - config['ACCESS_TOKEN'] = self.access_token - config['REFRESH_TOKEN'] = self.refresh_token - config['EMAIL'] = self.user_email + config["ACCESS_TOKEN"] = self.access_token + config["REFRESH_TOKEN"] = self.refresh_token + config["EMAIL"] = self.user_email if self.file_based_config: config_from_file(self.config_filename, config) else: self.config = config def update(self): - ''' Get new thermostat data from daikin skyport ''' + """Get new thermostat data from daikin skyport""" if self.skip_next: logger.debug("Skipping update due to setting change") self.skip_next = False @@ -314,12 +563,19 @@ def update(self): def make_request(self, index, body, log_msg_action, *, retry_count=0): self.skip_next = True - deviceID = self.thermostats[index]['id'] - url = 'https://api.daikinskyport.com/deviceData/' + deviceID - header = {'Content-Type': 'application/json;charset=UTF-8', - 'Authorization': 'Bearer ' + self.access_token} - logger.debug("Make Request: %s, Device: %s, Body: %s", log_msg_action, deviceID, body) - retry_strategy = Retry(total=8, backoff_factor=0.1,) + deviceID = self.thermostats[index]["id"] + url = "https://api.daikinskyport.com/deviceData/" + deviceID + header = { + "Content-Type": "application/json;charset=UTF-8", + "Authorization": "Bearer " + self.access_token, + } + logger.debug( + "Make Request: %s, Device: %s, Body: %s", log_msg_action, deviceID, body + ) + retry_strategy = Retry( + total=8, + backoff_factor=0.1, + ) adapter = HTTPAdapter(max_retries=retry_strategy) http = requests.Session() http.mount("https://", adapter) @@ -328,88 +584,100 @@ def make_request(self, index, body, log_msg_action, *, retry_count=0): try: request = http.put(url, headers=header, json=body) except RequestException as e: - logger.warn("Error connecting to Daikin Skyport. Possible connectivity outage: %s", e) + logger.warn( + "Error connecting to Daikin Skyport. Possible connectivity outage: %s", + e, + ) return None if request.status_code == requests.codes.ok: return request - elif (request.status_code == 401 and retry_count == 0 and - request.json()['error'] == 'authorization_expired'): + elif ( + request.status_code == 401 + and retry_count == 0 + and request.json()["error"] == "authorization_expired" + ): if self.refresh_tokens(): - return self.make_request(body, deviceID, log_msg_action, - retry_count=retry_count + 1) + return self.make_request( + body, deviceID, log_msg_action, retry_count=retry_count + 1 + ) else: logger.warn( "Error fetching data from Daikin Skyport while attempting to %s: %s", - log_msg_action, request.json()) + log_msg_action, + request.json(), + ) return None def set_hvac_mode(self, index, hvac_mode): - ''' possible modes are DAIKIN_HVAC_MODE_{OFF,HEAT,COOL,AUTO,AUXHEAT} ''' + """possible modes are DAIKIN_HVAC_MODE_{OFF,HEAT,COOL,AUTO,AUXHEAT}""" body = {"mode": hvac_mode} log_msg_action = "set HVAC mode" self.thermostats[index]["mode"] = hvac_mode return self.make_request(index, body, log_msg_action) - def set_thermostat_schedule(self, index, prefix, start, enable, label, heating, cooling): - ''' Schedule to set the thermostat. + def set_thermostat_schedule( + self, index, prefix, start, enable, label, heating, cooling + ): + """Schedule to set the thermostat. prefix is the beginning of the JSON key to modify. It consists of "sched" + [Mon,Tue,Wed,Thu,Fri,Sat,Sun] + "Part" + [1:6] (ex. schedMonPart1) start is the beginning of the schedule. It is an integer value where every 15 minutes from 00:00 is 1 (each hour = 4) enable is a boolean to set whether the schedule part is active or not label is a name for the part (ex. wakeup, work, etc.) heating is the heating set point for the part - cooling is the cooling set point for the part''' - body = {prefix + "Time": start, - prefix + "Enabled": enable, - prefix + "Label": label, - prefix + "hsp": heating, - prefix + "csp": cooling - } + cooling is the cooling set point for the part""" + body = { + prefix + "Time": start, + prefix + "Enabled": enable, + prefix + "Label": label, + prefix + "hsp": heating, + prefix + "csp": cooling, + } log_msg_action = "set thermostat schedule" return self.make_request(index, body, log_msg_action) def set_fan_mode(self, index, fan_mode): - ''' Set fan mode. Values: auto (0), schedule (2), on (1) ''' + """Set fan mode. Values: auto (0), schedule (2), on (1)""" body = {"fanCirculate": fan_mode} log_msg_action = "set fan mode" self.thermostats[index]["fanCirculate"] = fan_mode return self.make_request(index, body, log_msg_action) def set_fan_speed(self, index, fan_speed): - ''' Set fan speed. Values: low (0), medium (1), high (2) ''' + """Set fan speed. Values: low (0), medium (1), high (2)""" body = {"fanCirculateSpeed": fan_speed} log_msg_action = "set fan speed" self.thermostats[index]["fanCirculateSpeed"] = fan_speed return self.make_request(index, body, log_msg_action) def set_fan_clean(self, index, active): - ''' Enable/disable fan clean mode. This runs the fan at high speed to clear out the air. - active values are true/false''' + """Enable/disable fan clean mode. This runs the fan at high speed to clear out the air. + active values are true/false""" body = {"oneCleanFanActive": active} log_msg_action = "set fan clean mode" return self.make_request(index, body, log_msg_action) def set_dual_fuel_efficiency(self, index, active): - ''' Enable/disable dual fuel efficiency mode. This disables the use of aux heat above -5.5C/22F. - active values are true/false''' + """Enable/disable dual fuel efficiency mode. This disables the use of aux heat above -5.5C/22F. + active values are true/false""" body = {"ctDualFuelFurnaceLockoutEnable": active} log_msg_action = "set dual fuel efficiency mode" return self.make_request(index, body, log_msg_action) - def set_temp_hold(self, index, cool_temp=None, heat_temp=None, - hold_duration=None): - ''' Set a temporary hold ''' + def set_temp_hold(self, index, cool_temp=None, heat_temp=None, hold_duration=None): + """Set a temporary hold""" if hold_duration is None: hold_duration = self.thermostats[index]["schedOverrideDuration"] if cool_temp is None: cool_temp = self.thermostats[index]["cspHome"] if heat_temp is None: heat_temp = self.thermostats[index]["hspHome"] - body = {"hspHome": round(heat_temp, 1), - "cspHome": round(cool_temp, 1), - "schedOverride": 1, - "schedOverrideDuration": hold_duration - } + body = { + "hspHome": round(heat_temp, 1), + "cspHome": round(cool_temp, 1), + "schedOverride": 1, + "schedOverrideDuration": hold_duration, + } log_msg_action = "set hold temp" self.thermostats[index]["hspHome"] = round(heat_temp, 1) self.thermostats[index]["cspHome"] = round(cool_temp, 1) @@ -418,18 +686,19 @@ def set_temp_hold(self, index, cool_temp=None, heat_temp=None, return self.make_request(index, body, log_msg_action) def set_permanent_hold(self, index, cool_temp=None, heat_temp=None): - ''' Set a climate hold - ie enable/disable schedule. + """Set a climate hold - ie enable/disable schedule. active values are true/false - hold_duration is NEXT_SCHEDULE''' + hold_duration is NEXT_SCHEDULE""" if cool_temp is None: cool_temp = self.thermostats[index]["cspHome"] if heat_temp is None: heat_temp = self.thermostats[index]["hspHome"] - body = {"hspHome": round(heat_temp, 1), - "cspHome": round(cool_temp, 1), - "schedOverride": 0, - "schedEnabled": False - } + body = { + "hspHome": round(heat_temp, 1), + "cspHome": round(cool_temp, 1), + "schedOverride": 0, + "schedEnabled": False, + } log_msg_action = "set permanent hold" self.thermostats[index]["hspHome"] = round(heat_temp, 1) self.thermostats[index]["cspHome"] = round(cool_temp, 1) @@ -438,15 +707,12 @@ def set_permanent_hold(self, index, cool_temp=None, heat_temp=None): return self.make_request(index, body, log_msg_action) def set_away(self, index, mode, heat_temp=None, cool_temp=None): - ''' Enable/Disable the away setting and optionally set the away temps ''' + """Enable/Disable the away setting and optionally set the away temps""" if heat_temp is None: heat_temp = round(self.thermostats[index]["hspAway"], 1) if cool_temp is None: cool_temp = round(self.thermostats[index]["cspAway"], 1) - body = {"geofencingAway": mode, - "hspAway": heat_temp, - "cspAway": cool_temp - } + body = {"geofencingAway": mode, "hspAway": heat_temp, "cspAway": cool_temp} log_msg_action = "set away mode" self.thermostats[index]["geofencingAway"] = mode @@ -455,50 +721,46 @@ def set_away(self, index, mode, heat_temp=None, cool_temp=None): return self.make_request(index, body, log_msg_action) def resume_program(self, index): - ''' Resume currently scheduled program ''' - body = {"schedEnabled": True, - "schedOverride": 0, - "geofencingAway": False - } + """Resume currently scheduled program""" + body = {"schedEnabled": True, "schedOverride": 0, "geofencingAway": False} log_msg_action = "resume program" return self.make_request(index, body, log_msg_action) def set_fan_schedule(self, index, start, stop, interval, speed): - ''' Schedule to run the fan. + """Schedule to run the fan. start_time is the beginning of the schedule per day. It is an integer value where every 15 minutes from 00:00 is 1 (each hour = 4) end_time is the end of the schedule each day. Values are same as start_time - interval is the run time per hour of the schedule. Options are on the full time (0), 5mins (1), 15mins (2), 30mins (3), and 45mins (4) - speed is low (0) medium (1) or high (2)''' - body = {"fanCirculateStart": start, - "fanCirculateStop": stop, - "fanCirculateDuration": interval, - "fanCirculateSpeed": speed - } + interval is the run time per hour of the schedule. Options are on the full time (0), 5mins (1), 15mins (2), 30mins (3), and 45mins (4) + speed is low (0) medium (1) or high (2)""" + body = { + "fanCirculateStart": start, + "fanCirculateStop": stop, + "fanCirculateDuration": interval, + "fanCirculateSpeed": speed, + } log_msg_action = "set fan schedule" return self.make_request(index, body, log_msg_action) def set_night_mode(self, index, start, stop, enable): - ''' Set the night mode parameters ''' - body = {"nightModeStart": start, - "nightModeStop": stop, - "nightModeEnabled": enable, - } + """Set the night mode parameters""" + body = { + "nightModeStart": start, + "nightModeStop": stop, + "nightModeEnabled": enable, + } log_msg_action = "set night mode" return self.make_request(index, body, log_msg_action) def set_humidity(self, index, humidity_low=None, humidity_high=None): - ''' Set humidity level''' + """Set humidity level""" if humidity_low is None: humidity_low = self.thermostats[index]["humSP"] if humidity_high is None: humidity_high = self.thermostats[index]["dehumSP"] - body = {"dehumSP": humidity_high, - "humSP": humidity_low - } + body = {"dehumSP": humidity_high, "humSP": humidity_low} log_msg_action = "set humidity level" return self.make_request(index, body, log_msg_action) - diff --git a/custom_components/daikinskyport/sensor.py b/custom_components/daikinskyport/sensor.py index 84cb28f..6b78814 100644 --- a/custom_components/daikinskyport/sensor.py +++ b/custom_components/daikinskyport/sensor.py @@ -1,32 +1,26 @@ """Support for Daikin Skyport sensors.""" -from homeassistant.const import ( - PERCENTAGE, - UnitOfTemperature, - CONCENTRATION_PARTS_PER_MILLION, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - UnitOfPower, - UnitOfVolumeFlowRate -) + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, - SensorEntityDescription, SensorStateClass, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + UnitOfPower, + UnitOfTemperature, + UnitOfVolumeFlowRate, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo -from . import DaikinSkyportData +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - _LOGGER, - DOMAIN, - COORDINATOR, -) +from . import DaikinSkyportData +from .const import COORDINATOR, DOMAIN DEVICE_CLASS_DEMAND = "demand" DEVICE_CLASS_FAULT_CODE = "Code" @@ -127,6 +121,7 @@ }, } + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -138,12 +133,35 @@ async def async_setup_entry( for index in range(len(coordinator.daikinskyport.thermostats)): sensors = coordinator.daikinskyport.get_sensors(index) for sensor in sensors: - if sensor["type"] not in ("temperature", "humidity", "score", - "ozone", "particle", "VOC", "demand", - "power", "frequency_percent","actual_status", - "airflow", "fault_code") or sensor["value"] == 127.5 or sensor["value"] == 65535: + if ( + sensor["type"] + not in ( + "temperature", + "humidity", + "score", + "ozone", + "particle", + "VOC", + "demand", + "power", + "frequency_percent", + "actual_status", + "airflow", + "fault_code", + ) + or sensor["value"] == 127.5 + or sensor["value"] == 65535 + ): continue - async_add_entities([DaikinSkyportSensor(coordinator, sensor["name"], sensor["type"], index)], True) + async_add_entities( + [ + DaikinSkyportSensor( + coordinator, sensor["name"], sensor["type"], index + ) + ], + True, + ) + class DaikinSkyportSensor(SensorEntity): """Representation of a Daikin sensor.""" @@ -152,14 +170,18 @@ def __init__(self, data, sensor_name, sensor_type, sensor_index): """Initialize the sensor.""" self.data = data self._name = f"{sensor_name} {SENSOR_TYPES[sensor_type]['device_class']}" - self._attr_unique_id = f"{data.daikinskyport.thermostats[sensor_index]['id']}-{self._name}" + self._attr_unique_id = ( + f"{data.daikinskyport.thermostats[sensor_index]['id']}-{self._name}" + ) self._model = f"{data.daikinskyport.thermostats[sensor_index]['model']}" self._sensor_name = sensor_name self._type = sensor_type self._index = sensor_index self._state = None - self._native_unit_of_measurement = SENSOR_TYPES[sensor_type]["native_unit_of_measurement"] - self._attr_state_class = SENSOR_TYPES[sensor_type]['state_class'] + self._native_unit_of_measurement = SENSOR_TYPES[sensor_type][ + "native_unit_of_measurement" + ] + self._attr_state_class = SENSOR_TYPES[sensor_type]["state_class"] @property def device_info(self) -> DeviceInfo: diff --git a/custom_components/daikinskyport/services.yaml b/custom_components/daikinskyport/services.yaml index b49c071..2be33ee 100644 --- a/custom_components/daikinskyport/services.yaml +++ b/custom_components/daikinskyport/services.yaml @@ -171,5 +171,3 @@ daikin_prioritize_efficiency: example: "True" selector: boolean: - - diff --git a/custom_components/daikinskyport/strings.json b/custom_components/daikinskyport/strings.json index f6d8bec..e30999c 100644 --- a/custom_components/daikinskyport/strings.json +++ b/custom_components/daikinskyport/strings.json @@ -14,7 +14,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" @@ -43,7 +43,7 @@ }, "system_health": { "info": { - "can_reach_server": "Reach Daikin Skyport server", + "can_reach_server": "Reach Daikin Skyport server" } } -} \ No newline at end of file +} diff --git a/custom_components/daikinskyport/switch.py b/custom_components/daikinskyport/switch.py index b594a20..904ea8a 100644 --- a/custom_components/daikinskyport/switch.py +++ b/custom_components/daikinskyport/switch.py @@ -1,22 +1,23 @@ """Daikin Skyport switch""" + from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback - +from . import DaikinSkyportData from .const import ( _LOGGER, COORDINATOR, - DOMAIN, DAIKIN_HVAC_MODE_AUXHEAT, - DAIKIN_HVAC_MODE_HEAT + DAIKIN_HVAC_MODE_HEAT, + DOMAIN, ) -from . import DaikinSkyportData + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -28,7 +29,10 @@ async def async_setup_entry( for index in range(len(coordinator.daikinskyport.thermostats)): thermostat = coordinator.daikinskyport.get_thermostat(index) - async_add_entities([DaikinSkyportAuxHeat(coordinator, thermostat["name"], index)], True) + async_add_entities( + [DaikinSkyportAuxHeat(coordinator, thermostat["name"], index)], True + ) + class DaikinSkyportAuxHeat(SwitchEntity): """Representation of Daikin Skyport aux_heat data.""" @@ -40,7 +44,9 @@ def __init__(self, data, name, index): """Initialize the Daikin Skyport aux_heat platform.""" self.data = data self._name = f"{name} Aux Heat" - self._attr_unique_id = f"{data.daikinskyport.thermostats[index]['id']}-{self._name}" + self._attr_unique_id = ( + f"{data.daikinskyport.thermostats[index]['id']}-{self._name}" + ) self._index = index self.aux_on = False @@ -56,16 +62,23 @@ def is_on(self) -> bool: def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - send_command = self.data.daikinskyport.set_hvac_mode(self._index, DAIKIN_HVAC_MODE_AUXHEAT) + send_command = self.data.daikinskyport.set_hvac_mode( + self._index, DAIKIN_HVAC_MODE_AUXHEAT + ) if send_command: self.aux_on = True self.schedule_update_ha_state() else: - raise HomeAssistantError(f"Error {send_command}: Failed to turn on {self._name}") + raise HomeAssistantError( + f"Error {send_command}: Failed to turn on {self._name}" + ) def turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - if self.data.daikinskyport.get_thermostat(self._index)['mode'] == DAIKIN_HVAC_MODE_AUXHEAT: + if ( + self.data.daikinskyport.get_thermostat(self._index)["mode"] + == DAIKIN_HVAC_MODE_AUXHEAT + ): self.data.daikinskyport.set_hvac_mode(self._index, DAIKIN_HVAC_MODE_HEAT) self.aux_on = False self.schedule_update_ha_state() @@ -79,7 +92,7 @@ async def async_update(self) -> None: _LOGGER.debug("Updating switch entity") await self.data._async_update_data() thermostat = self.data.daikinskyport.get_thermostat(self._index) - if thermostat['mode'] == DAIKIN_HVAC_MODE_AUXHEAT: + if thermostat["mode"] == DAIKIN_HVAC_MODE_AUXHEAT: self.aux_on = True else: self.aux_on = False diff --git a/custom_components/daikinskyport/weather.py b/custom_components/daikinskyport/weather.py index f2ca9d4..2d9766f 100644 --- a/custom_components/daikinskyport/weather.py +++ b/custom_components/daikinskyport/weather.py @@ -1,37 +1,26 @@ """Support for displaying weather info from Daikin Skyport API.""" -from datetime import datetime, timedelta -from pytz import timezone, utc -import logging + +from datetime import timedelta from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_TIME, Forecast, WeatherEntity, WeatherEntityFeature, ) -from homeassistant.const import ( - UnitOfLength, - UnitOfPressure, - UnitOfSpeed, - UnitOfTemperature, -) -from homeassistant.util import dt as dt_util -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.core import HomeAssistant -from homeassistant.config_entries import ConfigEntry +from homeassistant.util import dt as dt_util -from .const import ( - _LOGGER, - DAIKIN_WEATHER_ICON_TO_HASS, - COORDINATOR, - DOMAIN, -) from . import DaikinSkyportData +from .const import _LOGGER, COORDINATOR, DAIKIN_WEATHER_ICON_TO_HASS, DOMAIN + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -43,7 +32,10 @@ async def async_setup_entry( for index in range(len(coordinator.daikinskyport.thermostats)): thermostat = coordinator.daikinskyport.get_thermostat(index) - async_add_entities([DaikinSkyportWeather(coordinator, thermostat["name"], index)], True) + async_add_entities( + [DaikinSkyportWeather(coordinator, thermostat["name"], index)], True + ) + class DaikinSkyportWeather(WeatherEntity): """Representation of Daikin Skyport weather data.""" @@ -57,25 +49,35 @@ def __init__(self, data, name, index): """Initialize the Daikin Skyport weather platform.""" self.data = data self._name = name - self._attr_unique_id = f"{data.daikinskyport.thermostats[index]['id']}-{self._name}" + self._attr_unique_id = ( + f"{data.daikinskyport.thermostats[index]['id']}-{self._name}" + ) self._index = index self.weather = None async def async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units. - + Only implement this method if `WeatherEntityFeature.FORECAST_DAILY` is set """ - + forecasts: list[Forecast] = [] date = dt_util.utcnow() for day in ["Today", "Day1", "Day2", "Day3", "Day4", "Day5"]: forecast = {} try: - forecast[ATTR_FORECAST_CONDITION] = DAIKIN_WEATHER_ICON_TO_HASS[self.weather["weather" + day + "Icon"]] - forecast[ATTR_FORECAST_NATIVE_TEMP] = self.weather["weather" + day + "TempC"] + forecast[ATTR_FORECAST_CONDITION] = DAIKIN_WEATHER_ICON_TO_HASS[ + self.weather["weather" + day + "Icon"] + ] + forecast[ATTR_FORECAST_NATIVE_TEMP] = self.weather[ + "weather" + day + "TempC" + ] forecast[ATTR_FORECAST_HUMIDITY] = self.weather["weather" + day + "Hum"] - _LOGGER.debug("Weather icon for weather%sIcon: %s", day, self.weather["weather" + day + "Icon"]) + _LOGGER.debug( + "Weather icon for weather%sIcon: %s", + day, + self.weather["weather" + day + "Icon"], + ) except (ValueError, IndexError, KeyError) as e: _LOGGER.error("Key not found for weather icon: %s", e) date += timedelta(days=1) @@ -100,7 +102,10 @@ def name(self): def condition(self): """Return the current condition.""" try: - _LOGGER.debug("Weather icon for weatherTodayIcon: %s", self.weather["weatherTodayIcon"]) + _LOGGER.debug( + "Weather icon for weatherTodayIcon: %s", + self.weather["weatherTodayIcon"], + ) return DAIKIN_WEATHER_ICON_TO_HASS[self.weather["weatherTodayIcon"]] except KeyError as e: _LOGGER.error("Key not found for weather condition: %s", e) @@ -132,6 +137,6 @@ async def async_update(self) -> None: self.weather = dict() thermostat = self.data.daikinskyport.get_thermostat(self._index) for key in thermostat: - if key.startswith('weather'): + if key.startswith("weather"): self.weather[key] = thermostat[key] self.weather["tz"] = thermostat["timeZone"]