From 5e668fba32f55b5444d55ccda2220608ee7dd593 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 10:32:17 -0600 Subject: [PATCH 1/9] flake8 fixes (some 'black' fixes also snuck in, mostly related to white space) No intentional functinoal changes. --- custom_components/daikinskyport/__init__.py | 62 +++++---------- custom_components/daikinskyport/climate.py | 75 +++++++++---------- .../daikinskyport/config_flow.py | 31 +++----- custom_components/daikinskyport/const.py | 45 +++++------ .../daikinskyport/daikinskyport.py | 30 ++++---- custom_components/daikinskyport/sensor.py | 8 +- custom_components/daikinskyport/switch.py | 2 + custom_components/daikinskyport/weather.py | 14 ++-- 8 files changed, 106 insertions(+), 161 deletions(-) diff --git a/custom_components/daikinskyport/__init__.py b/custom_components/daikinskyport/__init__.py index e70085f..d6566ee 100644 --- a/custom_components/daikinskyport/__init__.py +++ b/custom_components/daikinskyport/__init__.py @@ -1,27 +1,11 @@ """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.helpers.entity import DeviceInfo from .daikinskyport import DaikinSkyport, ExpiredTokenError @@ -41,6 +25,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 +35,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 +47,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 +75,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 +95,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 +104,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 +117,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 +133,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 +165,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..638464c 100644 --- a/custom_components/daikinskyport/climate.py +++ b/custom_components/daikinskyport/climate.py @@ -14,26 +14,21 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - PRESET_AWAY, FAN_AUTO, FAN_ON, FAN_LOW, FAN_MEDIUM, FAN_HIGH, - PRESET_NONE, ) 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 @@ -52,34 +47,34 @@ COORDINATOR, ) -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 +82,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 @@ -222,6 +217,7 @@ | ClimateEntityFeature.TURN_OFF ) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -234,7 +230,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 +238,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 +249,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 +258,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 +269,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 +277,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,7 +299,7 @@ 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: @@ -319,7 +315,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 +331,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 +382,7 @@ def set_efficiency_service(service): schema=EFFICIENCY_SCHEMA, ) + class Thermostat(ClimateEntity): """A thermostat class for Daikin Skyport Thermostats.""" @@ -424,7 +421,7 @@ def __init__(self, data, thermostat_index, thermostat): 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)): + 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) @@ -471,7 +468,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): @@ -563,7 +560,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,15 +569,15 @@ 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) @@ -625,7 +621,6 @@ def extra_state_attributes(self): "media_filter_days": self.thermostat["alertMediaAirFilterDays"] } - def set_preset_mode(self, preset_mode): """Activate a preset.""" if preset_mode == self.preset_mode: @@ -641,13 +636,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 @@ -682,10 +677,10 @@ def set_auto_temp_hold(self, heat_temp, cool_temp): 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, @@ -703,19 +698,19 @@ def set_fan_mode(self, 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._fan_mode = fan_mode _LOGGER.debug("Setting fan mode to: %s", fan_mode) @@ -724,7 +719,7 @@ def set_fan_mode(self, 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 @@ -734,7 +729,6 @@ def set_fan_mode(self, fan_mode): _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 +760,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) diff --git a/custom_components/daikinskyport/config_flow.py b/custom_components/daikinskyport/config_flow.py index c5a0ff3..a8db33f 100644 --- a/custom_components/daikinskyport/config_flow.py +++ b/custom_components/daikinskyport/config_flow.py @@ -1,12 +1,8 @@ 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 -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -14,12 +10,8 @@ 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,11 +23,12 @@ "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: @@ -59,16 +52,16 @@ async def async_step_user(self, user_input=None): ) 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..053b1f2 100644 --- a/custom_components/daikinskyport/const.py +++ b/custom_components/daikinskyport/const.py @@ -1,51 +1,42 @@ 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..06073b1 100644 --- a/custom_components/daikinskyport/daikinskyport.py +++ b/custom_components/daikinskyport/daikinskyport.py @@ -3,7 +3,6 @@ import json import os import logging -from time import sleep from requests.exceptions import RequestException from requests.adapters import HTTPAdapter @@ -15,11 +14,13 @@ 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''' if config: @@ -37,7 +38,7 @@ def config_from_file(filename, config=None): try: with open(filename, 'r') as fdesc: return json.loads(fdesc.read()) - except IOError as error: + except IOError: return False else: return {} @@ -68,7 +69,7 @@ def __init__(self, config_filename=None, user_email=None, user_password=None, co self.user_email = config['EMAIL'] else: logger.error("Email missing from config.") - if 'PASSWORD' in config: # PASSWORD is only needed during first login + if 'PASSWORD' in config: # PASSWORD is only needed during first login self.user_password = config['PASSWORD'] if 'ACCESS_TOKEN' in config: @@ -155,7 +156,7 @@ def get_thermostats(self): for thermostat in self.thermostatlist: overwrite = False thermostat_info = self.get_thermostat_info(thermostat['id']) - if thermostat_info == None: + if thermostat_info is None: continue thermostat_info['name'] = thermostat['name'] thermostat_info['id'] = thermostat['id'] @@ -171,8 +172,7 @@ def get_thermostats(self): 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 + raise ExpiredTokenError("Daikin Skyport token expired") def get_thermostat_info(self, deviceid): ''' Retrieve the device info for the specific device ''' @@ -197,8 +197,7 @@ def get_thermostat_info(self, deviceid): 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 + raise ExpiredTokenError("Daikin Skyport token expired") if request.status_code == requests.codes.ok: self.authenticated = True return request.json() @@ -206,8 +205,7 @@ def get_thermostat_info(self, deviceid): 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 + raise ExpiredTokenError("Daikin Skyport token expired") def get_thermostat(self, index): ''' Return a single thermostat based on index ''' @@ -263,7 +261,6 @@ def get_sensors(self, index): 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"}) @@ -332,8 +329,8 @@ def make_request(self, index, body, log_msg_action, *, retry_count=0): 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) @@ -418,7 +415,7 @@ 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''' if cool_temp is None: @@ -465,10 +462,10 @@ def resume_program(self, index): 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) + 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, @@ -501,4 +498,3 @@ def set_humidity(self, index, humidity_low=None, humidity_high=None): 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..43214ed 100644 --- a/custom_components/daikinskyport/sensor.py +++ b/custom_components/daikinskyport/sensor.py @@ -11,11 +11,8 @@ 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 @@ -23,7 +20,6 @@ from . import DaikinSkyportData from .const import ( - _LOGGER, DOMAIN, COORDINATOR, ) @@ -127,6 +123,7 @@ }, } + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -140,11 +137,12 @@ async def async_setup_entry( for sensor in sensors: if sensor["type"] not in ("temperature", "humidity", "score", "ozone", "particle", "VOC", "demand", - "power", "frequency_percent","actual_status", + "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) + class DaikinSkyportSensor(SensorEntity): """Representation of a Daikin sensor.""" diff --git a/custom_components/daikinskyport/switch.py b/custom_components/daikinskyport/switch.py index b594a20..ad8a1b5 100644 --- a/custom_components/daikinskyport/switch.py +++ b/custom_components/daikinskyport/switch.py @@ -18,6 +18,7 @@ ) from . import DaikinSkyportData + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -30,6 +31,7 @@ async def async_setup_entry( thermostat = coordinator.daikinskyport.get_thermostat(index) async_add_entities([DaikinSkyportAuxHeat(coordinator, thermostat["name"], index)], True) + class DaikinSkyportAuxHeat(SwitchEntity): """Representation of Daikin Skyport aux_heat data.""" diff --git a/custom_components/daikinskyport/weather.py b/custom_components/daikinskyport/weather.py index f2ca9d4..06e0e74 100644 --- a/custom_components/daikinskyport/weather.py +++ b/custom_components/daikinskyport/weather.py @@ -1,7 +1,5 @@ """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, @@ -13,13 +11,9 @@ 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.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.core import HomeAssistant @@ -33,6 +27,7 @@ ) from . import DaikinSkyportData + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -45,6 +40,7 @@ async def async_setup_entry( thermostat = coordinator.daikinskyport.get_thermostat(index) async_add_entities([DaikinSkyportWeather(coordinator, thermostat["name"], index)], True) + class DaikinSkyportWeather(WeatherEntity): """Representation of Daikin Skyport weather data.""" @@ -63,10 +59,10 @@ def __init__(self, data, name, index): 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"]: From 527d7e57e85943547c763e312dba662c976a8a04 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 10:48:31 -0600 Subject: [PATCH 2/9] black fixes No intentional functional change --- custom_components/daikinskyport/__init__.py | 1 + custom_components/daikinskyport/climate.py | 130 ++-- .../daikinskyport/config_flow.py | 26 +- .../daikinskyport/daikinskyport.py | 606 +++++++++++++----- custom_components/daikinskyport/sensor.py | 45 +- custom_components/daikinskyport/switch.py | 26 +- custom_components/daikinskyport/weather.py | 30 +- 7 files changed, 610 insertions(+), 254 deletions(-) diff --git a/custom_components/daikinskyport/__init__.py b/custom_components/daikinskyport/__init__.py index d6566ee..075b47b 100644 --- a/custom_components/daikinskyport/__init__.py +++ b/custom_components/daikinskyport/__init__.py @@ -1,4 +1,5 @@ """Daikin Skyport integration.""" + from datetime import timedelta from homeassistant.exceptions import ConfigEntryNotReady diff --git a/custom_components/daikinskyport/climate.py b/custom_components/daikinskyport/climate.py index 638464c..9e7c3ff 100644 --- a/custom_components/daikinskyport/climate.py +++ b/custom_components/daikinskyport/climate.py @@ -1,4 +1,5 @@ """Support for Daikin Skyport Thermostats.""" + import collections from datetime import datetime from typing import Optional @@ -9,7 +10,7 @@ ClimateEntity, ClimateEntityFeature, HVACMode, - HVACAction + HVACAction, ) from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, @@ -146,7 +147,7 @@ HOLD_1HR: 60, HOLD_2HR: 120, HOLD_4HR: 240, - HOLD_8HR: 480 + HOLD_8HR: 480, } SERVICE_RESUME_PROGRAM = "daikin_resume_program" @@ -156,11 +157,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( { @@ -168,7 +165,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, } ) @@ -303,7 +300,9 @@ def set_thermostat_schedule_service(service): 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 @@ -404,7 +403,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"]] @@ -420,19 +421,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"] is 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): @@ -448,7 +462,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"]] @@ -513,7 +529,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 @@ -579,24 +598,35 @@ def extra_state_attributes(self): 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: @@ -618,7 +648,7 @@ 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): @@ -666,17 +696,15 @@ 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 @@ -695,8 +723,7 @@ 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 @@ -707,8 +734,7 @@ def set_fan_mode(self, fan_mode): # 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 @@ -716,8 +742,7 @@ def set_fan_mode(self, 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] @@ -725,7 +750,9 @@ def set_fan_mode(self, fan_mode): _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 @@ -778,9 +805,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): @@ -809,7 +834,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() @@ -839,16 +873,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 a8db33f..ba5aa88 100644 --- a/custom_components/daikinskyport/config_flow.py +++ b/custom_components/daikinskyport/config_flow.py @@ -32,13 +32,19 @@ class DaikinSkyportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 @@ -47,10 +53,8 @@ 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", data_schema=vol.Schema( @@ -59,7 +63,7 @@ async def async_step_user(self, user_input=None): vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_NAME, default="Daikin"): str, } - ) + ), ) @staticmethod diff --git a/custom_components/daikinskyport/daikinskyport.py b/custom_components/daikinskyport/daikinskyport.py index 06073b1..65c8c6c 100644 --- a/custom_components/daikinskyport/daikinskyport.py +++ b/custom_components/daikinskyport/daikinskyport.py @@ -1,4 +1,5 @@ -''' Python Code for Communication with the Daikin Skyport Thermostat. This is taken mostly from pyecobee, so much credit to those contributors''' +"""Python Code for Communication with the Daikin Skyport Thermostat. This is taken mostly from pyecobee, so much credit to those contributors""" + import requests import json import os @@ -10,7 +11,7 @@ from .const import DAIKIN_PERCENT_MULTIPLIER -logger = logging.getLogger('daikinskyport') +logger = logging.getLogger("daikinskyport") NEXT_SCHEDULE = 1 @@ -22,11 +23,11 @@ class ExpiredTokenError(Exception): 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) @@ -36,7 +37,7 @@ 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: return False @@ -45,9 +46,11 @@ def config_from_file(filename, config=None): 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 @@ -60,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: @@ -109,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() @@ -148,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']) + 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: @@ -170,15 +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) + 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,86 +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) + 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) + 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"), @@ -285,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 @@ -311,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) @@ -325,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) @@ -415,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) @@ -435,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 @@ -452,49 +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 - } + 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 43214ed..7eff7c8 100644 --- a/custom_components/daikinskyport/sensor.py +++ b/custom_components/daikinskyport/sensor.py @@ -1,4 +1,5 @@ """Support for Daikin Skyport sensors.""" + from homeassistant.const import ( PERCENTAGE, UnitOfTemperature, @@ -6,7 +7,7 @@ CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, UnitOfPower, - UnitOfVolumeFlowRate + UnitOfVolumeFlowRate, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -135,12 +136,34 @@ 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): @@ -150,14 +173,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/switch.py b/custom_components/daikinskyport/switch.py index ad8a1b5..23a4fc1 100644 --- a/custom_components/daikinskyport/switch.py +++ b/custom_components/daikinskyport/switch.py @@ -1,4 +1,5 @@ """Daikin Skyport switch""" + from typing import Any from homeassistant.components.switch import SwitchEntity @@ -14,7 +15,7 @@ COORDINATOR, DOMAIN, DAIKIN_HVAC_MODE_AUXHEAT, - DAIKIN_HVAC_MODE_HEAT + DAIKIN_HVAC_MODE_HEAT, ) from . import DaikinSkyportData @@ -29,7 +30,9 @@ 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): @@ -42,7 +45,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 @@ -58,16 +63,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() @@ -81,7 +93,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 06e0e74..3248b73 100644 --- a/custom_components/daikinskyport/weather.py +++ b/custom_components/daikinskyport/weather.py @@ -1,4 +1,5 @@ """Support for displaying weather info from Daikin Skyport API.""" + from datetime import timedelta from homeassistant.components.weather import ( @@ -38,7 +39,9 @@ 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): @@ -53,7 +56,9 @@ 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 @@ -68,10 +73,18 @@ async def async_forecast_daily(self) -> list[Forecast] | None: 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) @@ -96,7 +109,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) @@ -128,6 +144,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"] From 6348c5defccec4a31d0844391214602f16674f55 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 10:57:03 -0600 Subject: [PATCH 3/9] isort fixes --- custom_components/daikinskyport/__init__.py | 10 +++---- custom_components/daikinskyport/climate.py | 27 ++++++++--------- .../daikinskyport/config_flow.py | 4 +-- custom_components/daikinskyport/const.py | 1 + .../daikinskyport/daikinskyport.py | 6 ++-- custom_components/daikinskyport/sensor.py | 29 +++++++++---------- custom_components/daikinskyport/switch.py | 7 ++--- custom_components/daikinskyport/weather.py | 19 ++++-------- 8 files changed, 45 insertions(+), 58 deletions(-) diff --git a/custom_components/daikinskyport/__init__.py b/custom_components/daikinskyport/__init__.py index 075b47b..da52530 100644 --- a/custom_components/daikinskyport/__init__.py +++ b/custom_components/daikinskyport/__init__.py @@ -2,22 +2,22 @@ from datetime import timedelta -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.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" diff --git a/custom_components/daikinskyport/climate.py b/custom_components/daikinskyport/climate.py index 9e7c3ff..c3adebe 100644 --- a/custom_components/daikinskyport/climate.py +++ b/custom_components/daikinskyport/climate.py @@ -4,23 +4,24 @@ 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, - HVACMode, HVACAction, + HVACMode, ) from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, FAN_AUTO, - FAN_ON, + FAN_HIGH, FAN_LOW, FAN_MEDIUM, - FAN_HIGH, + FAN_ON, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -28,24 +29,20 @@ STATE_ON, UnitOfTemperature, ) - -import homeassistant.helpers.config_validation as cv -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"] diff --git a/custom_components/daikinskyport/config_flow.py b/custom_components/daikinskyport/config_flow.py index ba5aa88..df51998 100644 --- a/custom_components/daikinskyport/config_flow.py +++ b/custom_components/daikinskyport/config_flow.py @@ -1,9 +1,9 @@ from __future__ import annotations -from requests.exceptions import RequestException +import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_NAME 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 ( diff --git a/custom_components/daikinskyport/const.py b/custom_components/daikinskyport/const.py index 053b1f2..351d25a 100644 --- a/custom_components/daikinskyport/const.py +++ b/custom_components/daikinskyport/const.py @@ -1,4 +1,5 @@ import logging + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, diff --git a/custom_components/daikinskyport/daikinskyport.py b/custom_components/daikinskyport/daikinskyport.py index 65c8c6c..adc8bbb 100644 --- a/custom_components/daikinskyport/daikinskyport.py +++ b/custom_components/daikinskyport/daikinskyport.py @@ -1,12 +1,12 @@ """Python Code for Communication with the Daikin Skyport Thermostat. This is taken mostly from pyecobee, so much credit to those contributors""" -import requests import json -import os import logging +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 diff --git a/custom_components/daikinskyport/sensor.py b/custom_components/daikinskyport/sensor.py index 7eff7c8..6b78814 100644 --- a/custom_components/daikinskyport/sensor.py +++ b/custom_components/daikinskyport/sensor.py @@ -1,29 +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, SensorStateClass, ) -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 ( - DOMAIN, - COORDINATOR, -) +from . import DaikinSkyportData +from .const import COORDINATOR, DOMAIN DEVICE_CLASS_DEMAND = "demand" DEVICE_CLASS_FAULT_CODE = "Code" diff --git a/custom_components/daikinskyport/switch.py b/custom_components/daikinskyport/switch.py index 23a4fc1..904ea8a 100644 --- a/custom_components/daikinskyport/switch.py +++ b/custom_components/daikinskyport/switch.py @@ -5,19 +5,18 @@ 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, + DOMAIN, ) -from . import DaikinSkyportData async def async_setup_entry( diff --git a/custom_components/daikinskyport/weather.py b/custom_components/daikinskyport/weather.py index 3248b73..2d9766f 100644 --- a/custom_components/daikinskyport/weather.py +++ b/custom_components/daikinskyport/weather.py @@ -4,29 +4,22 @@ 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 ( - UnitOfTemperature, -) -from homeassistant.util import dt as dt_util +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( From f0909b2d4130ecf6ca86e59bd8bb3b37d863e3e3 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 14:08:09 -0600 Subject: [PATCH 4/9] Import config for flake8 --- .flake8 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .flake8 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 From c018702ee7755287584025f457bbb3654a2b1a97 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 14:08:21 -0600 Subject: [PATCH 5/9] Import config for isort --- .isort.cfg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .isort.cfg 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 From e671937eaf2d323d6172a0bf7f8a6d8803e2f1ad Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 15:58:02 -0600 Subject: [PATCH 6/9] Import pre-commit config --- .pre-commit-config.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .pre-commit-config.yaml 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 From e3cc170905b6f8ebf37e0f3300690e5f839f36a5 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Sun, 3 Aug 2025 15:56:59 -0600 Subject: [PATCH 7/9] GHA for pre-commit --- .github/workflows/pre-commit.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/pre-commit.yml 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 From 2aeb17ca3f2d73e8f90bef036631136688281224 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Tue, 5 Aug 2025 22:48:14 -0600 Subject: [PATCH 8/9] whitespace fixes --- API_info.md | 4 ++-- custom_components/daikinskyport/services.yaml | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) 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/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: - - From 44b5ddb0c526331e40a3a4198e298f3aeb185274 Mon Sep 17 00:00:00 2001 From: Joel Knight Date: Tue, 5 Aug 2025 22:48:26 -0600 Subject: [PATCH 9/9] JSON syntax fixes --- custom_components/daikinskyport/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 +}