diff --git a/custom_components/daikin_residential/__init__.py b/custom_components/daikin_residential/__init__.py new file mode 100644 index 0000000..3534234 --- /dev/null +++ b/custom_components/daikin_residential/__init__.py @@ -0,0 +1,229 @@ +"""Platform for the Daikin AC.""" +import asyncio +import datetime +import logging +import requests +import json +import time +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +# from homeassistant.exceptions import ConfigEntryNotReady +# import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import Throttle + +from .const import DOMAIN, DAIKIN_API, DAIKIN_DEVICES + +from .daikin_base import Appliance + +_LOGGER = logging.getLogger(__name__) + +ENTRY_IS_SETUP = "daikin_entry_is_setup" + +PARALLEL_UPDATES = 0 + +SERVICE_FORCE_UPDATE = "force_update" +SERVICE_PULL_DEVICES = "pull_devices" + +SIGNAL_DELETE_ENTITY = "daikin_delete" +SIGNAL_UPDATE_ENTITY = "daikin_update" + +TOKENSET_FILE = "tokenset.json" + +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=15) +REQUEST_DELAY_TIME_s = 5 + +COMPONENT_TYPES = ["climate", "sensor", "switch"] +# COMPONENT_TYPES = ["sensor", "switch"] + + +CONFIG_SCHEMA = vol.Schema(vol.All({DOMAIN: vol.Schema({})}), extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Establish connection with Daikin.""" + if DOMAIN not in config: + return True + + conf = config.get(DOMAIN) + if conf is not None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Establish connection with Daikin.""" + + daikin_api = DaikinApi(hass) + await daikin_api.getCloudDeviceDetails() + + devices = await daikin_api.getCloudDevices() + hass.data[DOMAIN] = {DAIKIN_API: daikin_api, DAIKIN_DEVICES: devices} + + for component in COMPONENT_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in COMPONENT_TYPES + ] + ) + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return True + + +async def daikin_api_setup(hass, host, key, uuid, password): + """Create a Daikin instance only once.""" + return + + +class DaikinApi: + """Daikin Residential API.""" + + def __init__(self, hass): + """Initialize a new Daikin Residential API.""" + # super() + self.hass = hass + self.tokenSet = None + self._delay_next_request = False + tokenFile = self.hass.config.path(TOKENSET_FILE) + _LOGGER.info("Initialing Daikin Residential API (%s)...", tokenFile) + try: + with open(tokenFile) as jsonFile: + jsonObject = json.load(jsonFile) + self.tokenSet = jsonObject + jsonFile.close() + except IOError: + _LOGGER.error("tokenset.json file not found in config folder.") + raise Exception("tokenset.json file not found in config folder.") + + _LOGGER.info("Initialized Daikin Residential API") + _LOGGER.debug( + "Daikin Residential API token is [%s]", self.tokenSet["access_token"] + ) + + async def doBearerRequest(self, resourceUrl, options=None, refreshed=False): + if self.tokenSet is None: + raise Exception( + "Please provide a TokenSet or use the Proxy server to Authenticate once" + ) + + if not resourceUrl.startswith("http"): + resourceUrl = "https://api.prod.unicloud.edc.dknadmin.be" + resourceUrl + + headers = { + "user-agent": "Daikin/1.6.1.4681 CFNetwork/1209 Darwin/20.2.0", + "x-api-key": "xw6gvOtBHq5b1pyceadRp6rujSNSZdjx2AqT03iC", + "Authorization": "Bearer " + self.tokenSet["access_token"], + "Content-Type": "application/json", + } + + _LOGGER.debug("BEARER REQUEST URL: %s", resourceUrl) + _LOGGER.debug("BEARER REQUEST HEADERS: %s", headers) + if options is not None and "method" in options and options["method"] == "PATCH": + _LOGGER.debug("BEARER REQUEST JSON: %s", options["json"]) + res = requests.patch(resourceUrl, headers=headers, data=options["json"]) + else: + res = requests.get(resourceUrl, headers=headers) + _LOGGER.debug("BEARER RESPONSE CODE: %s", res.status_code) + + if res.status_code == 200: + try: + return res.json() + except Exception: + return res.text + if res.status_code == 204: + return True + + if not refreshed and res.status_code == 401: + _LOGGER.info("TOKEN EXPIRED: will refresh it (%s)", res.status_code) + await self.refreshAccessToken() + return await self.doBearerRequest(resourceUrl, options, True) + + raise Exception("Communication failed! Status: " + str(res.status_code)) + + async def refreshAccessToken(self): + """Attempt to refresh the Access Token.""" + url = "https://cognito-idp.eu-west-1.amazonaws.com" + + headers = { + "Content-Type": "application/x-amz-json-1.1", + "x-amz-target": "AWSCognitoIdentityProviderService.InitiateAuth", + "x-amz-user-agent": "aws-amplify/0.1.x react-native", + "User-Agent": "Daikin/1.6.1.4681 CFNetwork/1220.1 Darwin/20.3.0", + } + ref_json = { + "ClientId": "7rk39602f0ds8lk0h076vvijnb", + "AuthFlow": "REFRESH_TOKEN_AUTH", + "AuthParameters": {"REFRESH_TOKEN": self.tokenSet["refresh_token"]}, + } + res = requests.post(url, headers=headers, json=ref_json) + _LOGGER.info("REFRESHACCESSTOKEN RESPONSE CODE: %s", res.status_code) + _LOGGER.debug("REFRESHACCESSTOKEN RESPONSE: %s", res.json()) + res_json = res.json() + + if ( + res_json["AuthenticationResult"] is not None + and res_json["AuthenticationResult"]["AccessToken"] is not None + and res_json["AuthenticationResult"]["TokenType"] == "Bearer" + ): + self.tokenSet["access_token"] = res_json["AuthenticationResult"][ + "AccessToken" + ] + self.tokenSet["id_token"] = res_json["AuthenticationResult"]["IdToken"] + self.tokenSet["expires_at"] = int( + datetime.datetime.now().timestamp() + ) + int(res_json["AuthenticationResult"]["ExpiresIn"]) + _LOGGER.debug("NEW TOKENSET: %s", self.tokenSet) + tokenFile = self.hass.config.path(TOKENSET_FILE) + with open(tokenFile, "w") as outfile: + json.dump(self.tokenSet, outfile) + + # self.emit('token_update', self.tokenSet); + return self.tokenSet + raise Exception( + "Token refresh was not successful! Status: " + str(res.status_code) + ) + + async def getApiInfo(self): + """Get Daikin API Info.""" + return await self.doBearerRequest("/v1/info") + + async def getCloudDeviceDetails(self): + """Get pure Device Data from the Daikin cloud devices.""" + return await self.doBearerRequest("/v1/gateway-devices") + + async def getCloudDevices(self): + """Get array of DaikinResidentialDevice objects and get their data.""" + devices = await self.getCloudDeviceDetails() + res = {} + for dev in devices or []: + res[dev["id"]] = Appliance(dev, self) + return res + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self, **kwargs): + """Pull the latest data from Daikin.""" + _LOGGER.debug("API UPDATE") + if self._delay_next_request: + _LOGGER.debug("SLEEPING FOR %i secs.", REQUEST_DELAY_TIME_s) + time.sleep(REQUEST_DELAY_TIME_s) + self._delay_next_request = False + + json_data = await self.getCloudDeviceDetails() + for dev_data in json_data or []: + self.hass.data[DOMAIN][DAIKIN_DEVICES][dev_data["id"]].setJsonData(dev_data) diff --git a/custom_components/daikin_residential/climate.py b/custom_components/daikin_residential/climate.py new file mode 100644 index 0000000..2b92b6f --- /dev/null +++ b/custom_components/daikin_residential/climate.py @@ -0,0 +1,268 @@ +"""Support for the Daikin HVAC.""" +import logging + +import voluptuous as vol + +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, +) +from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS +import homeassistant.helpers.config_validation as cv + +from .const import ( + DOMAIN as DAIKIN_DOMAIN, + DAIKIN_DEVICES, + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + ATTR_STATE_OFF, + ATTR_STATE_ON, + ATTR_TARGET_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string} +) + + +PRESET_MODES = {PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY} + +HA_HVAC_TO_DAIKIN = { + HVAC_MODE_FAN_ONLY: "fanOnly", + HVAC_MODE_DRY: "dry", + HVAC_MODE_COOL: "cooling", + HVAC_MODE_HEAT: "heating", + HVAC_MODE_HEAT_COOL: "auto", + HVAC_MODE_OFF: "off", +} + + +HA_ATTR_TO_DAIKIN = { + ATTR_PRESET_MODE: "en_hol", + ATTR_HVAC_MODE: "mode", + ATTR_FAN_MODE: "f_rate", + ATTR_SWING_MODE: "f_dir", + ATTR_INSIDE_TEMPERATURE: "htemp", + ATTR_OUTSIDE_TEMPERATURE: "otemp", + ATTR_TARGET_TEMPERATURE: "stemp", +} + +DAIKIN_ATTR_ADVANCED = "adv" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the Daikin HVAC platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Daikin climate based on config_entry.""" + for dev_id, device in hass.data[DAIKIN_DOMAIN][DAIKIN_DEVICES].items(): + async_add_entities([DaikinClimate(device)], update_before_add=True) + # daikin_api = hass.data[DAIKIN_DOMAIN].get(entry.entry_id) + # async_add_entities([DaikinClimate(daikin_api)], update_before_add=True) + + +class DaikinClimate(ClimateEntity): + """Representation of a Daikin HVAC.""" + + def __init__(self, device): + """Initialize the climate device.""" + + self._device = device + self._list = { + ATTR_HVAC_MODE: list(HA_HVAC_TO_DAIKIN), + ATTR_SWING_MODE: self._device.swing_modes, + } + + self._supported_features = SUPPORT_TARGET_TEMPERATURE + + self._supported_preset_modes = [PRESET_NONE] + self._current_preset_mode = PRESET_NONE + for mode in PRESET_MODES: + if self._device.support_preset_mode(mode): + self._supported_preset_modes.append(mode) + self._supported_features |= SUPPORT_PRESET_MODE + + if self._device.support_fan_rate: + self._supported_features |= SUPPORT_FAN_MODE + + if self._device.support_swing_mode: + self._supported_features |= SUPPORT_SWING_MODE + + async def _set(self, settings): + """Set device settings using API.""" + values = {} + + for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE]: + value = settings.get(attr) + if value is None: + continue + + daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) + if daikin_attr is not None: + if attr == ATTR_HVAC_MODE: + values[daikin_attr] = HA_HVAC_TO_DAIKIN[value] + elif value in self._list[attr]: + values[daikin_attr] = value.lower() + else: + _LOGGER.error("Invalid value %s for %s", attr, value) + + # temperature + elif attr == ATTR_TEMPERATURE: + try: + values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = str(int(value)) + except ValueError: + _LOGGER.error("Invalid temperature %s", value) + + if values: + await self._device.set(values) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._supported_features + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._device.name + + @property + def unique_id(self): + """Return a unique ID.""" + devID = self._device.getId() + return f"{devID}" + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.inside_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._device.target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + stepVal = self._device.target_temperature_step + return stepVal + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + await self._device.async_set_temperature(kwargs[ATTR_TEMPERATURE]) + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self._device.hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return self._device.hvac_modes + + async def async_set_hvac_mode(self, hvac_mode): + """Set HVAC mode.""" + await self._device.async_set_hvac_mode(HA_HVAC_TO_DAIKIN[hvac_mode]) + + @property + def fan_mode(self): + """Return the fan setting.""" + return self._device.fan_mode + + @property + def fan_modes(self): + """List of available fan modes.""" + return self._device.fan_modes + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + await self._device.async_set_fan_mode(fan_mode) + + @property + def swing_mode(self): + """Return the swing setting.""" + return self._device.swing_mode + + @property + def swing_modes(self): + """List of available swing modes.""" + return self._device.swing_modes + + async def async_set_swing_mode(self, swing_mode): + """Set new swing mode.""" + await self._device.async_set_swing_mode(swing_mode) + + @property + def preset_mode(self): + """Return the preset_mode.""" + self._current_preset_mode = PRESET_NONE + for mode in self._supported_preset_modes: + if self._device.preset_mode_status(mode) == ATTR_STATE_ON: + self._current_preset_mode = mode + return self._current_preset_mode + + async def async_set_preset_mode(self, preset_mode): + """Set preset mode.""" + curr_mode = self.preset_mode + if curr_mode != PRESET_NONE: + print("SETTING OFF {}".format(curr_mode)) + await self._device.set_preset_mode_status(curr_mode, ATTR_STATE_OFF) + if preset_mode != PRESET_NONE: + print("SETTING ON {}".format(preset_mode)) + await self._device.set_preset_mode_status(preset_mode, ATTR_STATE_ON) + + @property + def preset_modes(self): + """List of available preset modes.""" + return self._supported_preset_modes + + async def async_update(self): + """Retrieve latest state.""" + await self._device.api.async_update() + + async def async_turn_on(self): + """Turn device on.""" + await self._device.set({}) + + async def async_turn_off(self): + """Turn device off.""" + await self._device.set( + {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_HVAC_TO_DAIKIN[HVAC_MODE_OFF]} + ) + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._device.device_info() diff --git a/custom_components/daikin_residential/config_flow.py b/custom_components/daikin_residential/config_flow.py new file mode 100644 index 0000000..58c7c27 --- /dev/null +++ b/custom_components/daikin_residential/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for the Daikin platform.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries + +from .__init__ import DaikinApi +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class FlowHandler(config_entries.ConfigFlow): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the Daikin config flow.""" + self.host = None + + @property + def schema(self): + """Return current schema.""" + return vol.Schema({}) + + async def _create_entry(self): + """Register new entry.""" + # if not self.unique_id: + # await self.async_set_unique_id(password) + # self._abort_if_unique_id_configured() + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + await self.async_set_unique_id("DaikinResidentialController") + + return self.async_create_entry(title="daikin", data={}) + + async def _attempt_connection(self): + """Create device.""" + try: + daikin_api = DaikinApi(self.hass) + except Exception: + return self.async_abort(reason="missing_config") + try: + await daikin_api.getApiInfo() + except Exception: + return self.async_abort(reason="cannot_connect") + + return await self._create_entry() + + async def async_step_user(self, user_input=None): + """User initiated config flow.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=self.schema) + return await self._attempt_connection() + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self._create_entry() diff --git a/custom_components/daikin_residential/const.py b/custom_components/daikin_residential/const.py new file mode 100644 index 0000000..1b63576 --- /dev/null +++ b/custom_components/daikin_residential/const.py @@ -0,0 +1,141 @@ +"""Constants for Daikin Residential Controller.""" + +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, +) + +DOMAIN = "daikin_residential" + +DAIKIN_DATA = "daikin_data" +DAIKIN_API = "daikin_api" +DAIKIN_DEVICES = "daikin_devices" +DAIKIN_DISCOVERY_NEW = "daikin_discovery_new_{}" + +ATTR_ON_OFF = "on_off" +ATTR_PRESET_MODE = "preset_mode" +ATTR_OPERATION_MODE = "operation_mode" +ATTR_TEMPERATURE = "temperature" +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_INSIDE_TEMPERATURE = "inside_temperature" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_ENERGY_CONSUMPTION = "energy_consumption" +ATTR_HUMIDITY = "humidity" +ATTR_TARGET_HUMIDITY = "target_humidity" +ATTR_FAN_MODE = "fan_mode" +ATTR_FAN_SPEED = "fan_speed" +ATTR_HSWING_MODE = "hswing_mode" +ATTR_VSWING_MODE = "vswing_mode" +ATTR_SWING_AUTO = "auto" +ATTR_SWING_SWING = "swing" +ATTR_SWING_STOP = "stop" +ATTR_COOL_ENERGY = "cool_energy" +ATTR_HEAT_ENERGY = "heat_energy" + +MP_CLIMATE = "climateControl" +DP_ON_OFF = "onOffMode" +DP_OPERATION_MODE = "operationMode" +DP_SENSORS = "sensoryData" +DP_TEMPERATURE = "temperatureControl" +DP_FAN = "fanControl" +DP_CONSUMPTION = "consumptionData" + +DAIKIN_CMD_SETS = { + ATTR_ON_OFF: [MP_CLIMATE, DP_ON_OFF, ""], + ATTR_PRESET_MODE: [MP_CLIMATE, "", ""], + ATTR_OPERATION_MODE: [MP_CLIMATE, DP_OPERATION_MODE, ""], + ATTR_OUTSIDE_TEMPERATURE: [MP_CLIMATE, DP_SENSORS, "/outdoorTemperature"], + ATTR_INSIDE_TEMPERATURE: [MP_CLIMATE, DP_SENSORS, "/roomTemperature"], + ATTR_TARGET_TEMPERATURE: [ + MP_CLIMATE, + DP_TEMPERATURE, + "/operationModes/%operationMode%/setpoints/roomTemperature", + ], + ATTR_FAN_MODE: [ + MP_CLIMATE, + DP_FAN, + "/operationModes/%operationMode%/fanSpeed/currentMode", + ], + ATTR_FAN_SPEED: [ + MP_CLIMATE, + DP_FAN, + "/operationModes/%operationMode%/fanSpeed/modes/fixed", + ], + ATTR_HSWING_MODE: [ + MP_CLIMATE, + DP_FAN, + "/operationModes/%operationMode%/fanDirection/horizontal/currentMode", + ], + ATTR_VSWING_MODE: [ + MP_CLIMATE, + DP_FAN, + "/operationModes/%operationMode%/fanDirection/vertical/currentMode", + ], + ATTR_ENERGY_CONSUMPTION: [MP_CLIMATE, DP_CONSUMPTION, "/electrical"], +} + +ATTR_STATE_ON = "on" +ATTR_STATE_OFF = "off" + +FAN_FIXED = "fixed" +FAN_QUIET = "Silence" + +SWING_OFF = "Off" +SWING_BOTH = "3D" +SWING_VERTICAL = "Vertical" +SWING_HORIZONTAL = "Horizontal" + +PRESET_STREAMER = "streamer" + +SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_POWER = "power" +SENSOR_TYPE_ENERGY = "energy" +SENSOR_PERIOD_DAILY = "d" +SENSOR_PERIOD_WEEKLY = "w" +SENSOR_PERIOD_YEARLY = "m" +SENSOR_PERIODS = { + SENSOR_PERIOD_DAILY: "Daily", + SENSOR_PERIOD_WEEKLY: "Weekly", + SENSOR_PERIOD_YEARLY: "Yearly", +} + +SENSOR_TYPES = { + ATTR_INSIDE_TEMPERATURE: { + CONF_NAME: "Inside Temperature", + CONF_TYPE: SENSOR_TYPE_TEMPERATURE, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ATTR_OUTSIDE_TEMPERATURE: { + CONF_NAME: "Outside Temperature", + CONF_TYPE: SENSOR_TYPE_TEMPERATURE, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ATTR_COOL_ENERGY: { + CONF_NAME: "Cool Energy Consumption", + CONF_TYPE: SENSOR_TYPE_ENERGY, + CONF_ICON: "mdi:snowflake", + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + }, + ATTR_HEAT_ENERGY: { + CONF_NAME: "Heat Energy Consumption", + CONF_TYPE: SENSOR_TYPE_ENERGY, + CONF_ICON: "mdi:fire", + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + }, +} + +CONF_UUID = "uuid" + +KEY_MAC = "mac" +KEY_IP = "ip" + +TIMEOUT = 60 diff --git a/custom_components/daikin_residential/daikin_base.py b/custom_components/daikin_residential/daikin_base.py new file mode 100644 index 0000000..d36118c --- /dev/null +++ b/custom_components/daikin_residential/daikin_base.py @@ -0,0 +1,322 @@ +"""Pydaikin base appliance, represent a Daikin device.""" + +import logging + +from .device import DaikinResidentialDevice + +from .const import ( + PRESET_STREAMER, + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + ATTR_STATE_OFF, + ATTR_STATE_ON, + ATTR_TARGET_TEMPERATURE, + FAN_QUIET, + SWING_OFF, + SWING_BOTH, + SWING_VERTICAL, + SWING_HORIZONTAL, + DAIKIN_CMD_SETS, + ATTR_ON_OFF, + ATTR_OPERATION_MODE, + ATTR_FAN_SPEED, + ATTR_HSWING_MODE, + ATTR_VSWING_MODE, + ATTR_SWING_SWING, + ATTR_SWING_STOP, + ATTR_ENERGY_CONSUMPTION, + SENSOR_PERIOD_WEEKLY, +) + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_PRESET_MODE, + FAN_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, +) + +_LOGGER = logging.getLogger(__name__) + +HA_PRESET_TO_DAIKIN = { + PRESET_AWAY: "holidayMode", + PRESET_NONE: "off", + PRESET_BOOST: "powerfulMode", + PRESET_COMFORT: "comfortMode", + PRESET_ECO: "econoMode", + PRESET_STREAMER: "streamerMode", +} + +DAIKIN_HVAC_TO_HA = { + "fanOnly": HVAC_MODE_FAN_ONLY, + "dry": HVAC_MODE_DRY, + "cooling": HVAC_MODE_COOL, + "heating": HVAC_MODE_HEAT, + "auto": HVAC_MODE_HEAT_COOL, + "off": HVAC_MODE_OFF, +} + +DAIKIN_FAN_TO_HA = {"auto": FAN_AUTO, "quiet": FAN_QUIET} + +HA_FAN_TO_DAIKIN = { + DAIKIN_FAN_TO_HA["auto"]: "auto", + DAIKIN_FAN_TO_HA["quiet"]: "quiet", +} + + +class Appliance(DaikinResidentialDevice): # pylint: disable=too-many-public-methods + """Daikin main appliance class.""" + + @staticmethod + def translate_mac(value): + """Return translated MAC address.""" + return ":".join(value[i : i + 2] for i in range(0, len(value), 2)) + + def __init__(self, jsonData, apiInstance): + """Init the pydaikin appliance, representing one Daikin device.""" + super().__init__(jsonData, apiInstance) + + async def init(self): + """Init status.""" + # Re-defined in all sub-classes + raise NotImplementedError + + def getCommandSet(self, param): + if param in HA_PRESET_TO_DAIKIN.values(): + cmd_set = DAIKIN_CMD_SETS[ATTR_PRESET_MODE].copy() + cmd_set[1] = param + else: + cmd_set = DAIKIN_CMD_SETS[param].copy() + if "%operationMode%" in cmd_set[2]: + operation_mode = self.getValue(ATTR_OPERATION_MODE) + cmd_set[2] = cmd_set[2].replace("%operationMode%", operation_mode) + return cmd_set + + def getData(self, param): + """Get the current data of a data object.""" + cmd_set = self.getCommandSet(param) + return self.get_data(cmd_set[0], cmd_set[1], cmd_set[2]) + + def getValue(self, param): + """Get the current value of a data object.""" + return self.getData(param)["value"] + + def getValidValues(self, param): + """Get the valid values of a data object.""" + return self.getData(param)["values"] + + async def setValue(self, param, value): + """Get the current value of a data object.""" + cmd_set = self.getCommandSet(param) + return await self.set_data(cmd_set[0], cmd_set[1], cmd_set[2], value) + + @property + def mac(self): + """Return device's MAC address.""" + return self.get_value("gateway", "macAddress") + + @property + def hvac_mode(self): + """Return current HVAC mode.""" + mode = HVAC_MODE_OFF + if self.getValue(ATTR_ON_OFF) != ATTR_STATE_OFF: + mode = self.getValue(ATTR_OPERATION_MODE) + return DAIKIN_HVAC_TO_HA.get(mode, HVAC_MODE_HEAT_COOL) + + @property + def hvac_modes(self): + """Return the list of available HVAC modes.""" + modes = [HVAC_MODE_OFF] + for mode in self.getValidValues(ATTR_OPERATION_MODE): + modes.append(DAIKIN_HVAC_TO_HA[mode]) + return modes + + async def async_set_hvac_mode(self, hvac_mode): + """Set HVAC mode.""" + if hvac_mode == HVAC_MODE_OFF: + return await self.setValue(ATTR_ON_OFF, ATTR_STATE_OFF) + else: + if self.hvac_mode == HVAC_MODE_OFF: + await self.setValue(ATTR_ON_OFF, ATTR_STATE_ON) + return await self.setValue(ATTR_OPERATION_MODE, hvac_mode) + + def support_preset_mode(self, mode): + """Return True if the device supports preset mode.""" + mode = HA_PRESET_TO_DAIKIN[mode] + return self.getData(mode) is not None + + def preset_mode_status(self, mode): + """Return the preset mode status.""" + mode = HA_PRESET_TO_DAIKIN[mode] + if self.getData(mode) is None: + return False + return self.getValue(mode) + + def set_preset_mode_status(self, mode, status): + """Set the preset mode status.""" + mode = HA_PRESET_TO_DAIKIN[mode] + if self.getData(mode) is None: + return + return self.setValue(mode, status) + + @property + def support_fan_rate(self): + """Return True if the device support setting fan_rate.""" + return True + + @property + def fan_mode(self): + """Return current fan mode.""" + fanMode = self.getValue(ATTR_FAN_MODE) + if fanMode in DAIKIN_FAN_TO_HA: + fanMode = DAIKIN_FAN_TO_HA[fanMode] + else: + fanMode = self.getValue(ATTR_FAN_SPEED) + return fanMode + + @property + def fan_modes(self): + """Return available fan modes for current HVAC mode.""" + fanModes = [] + modes = self.getValidValues(ATTR_FAN_MODE) + for val in modes: + if val in DAIKIN_FAN_TO_HA: + fanModes.append(DAIKIN_FAN_TO_HA[val]) + else: + fixedModes = self.getData(ATTR_FAN_SPEED) + minVal = int(fixedModes["minValue"]) + maxVal = int(fixedModes["maxValue"]) + for val in range(minVal, maxVal + 1): + fanModes.append(str(val)) + return fanModes + + async def async_set_fan_mode(self, mode): + """Set the preset mode status.""" + if mode in HA_FAN_TO_DAIKIN.keys(): + return await self.setValue(ATTR_FAN_MODE, HA_FAN_TO_DAIKIN[mode]) + if mode.isnumeric(): + mode = int(mode) + return await self.setValue(ATTR_FAN_SPEED, mode) + + @property + def support_swing_mode(self): + """Return True if the device support setting swing_mode.""" + return self.getData(ATTR_VSWING_MODE) is not None + + @property + def swing_mode(self): + swingMode = SWING_OFF + hMode = self.getValue(ATTR_HSWING_MODE) + vMode = self.getValue(ATTR_VSWING_MODE) + if hMode != ATTR_SWING_STOP: + swingMode = SWING_HORIZONTAL + if vMode != ATTR_SWING_STOP: + if hMode != ATTR_SWING_STOP: + swingMode = SWING_BOTH + else: + swingMode = SWING_VERTICAL + return swingMode + + @property + def swing_modes(self): + """Return list of supported swing modes.""" + swingModes = [SWING_OFF] + hMode = self.getData(ATTR_HSWING_MODE) + vMode = self.getData(ATTR_VSWING_MODE) + if hMode is not None: + swingModes.append(SWING_HORIZONTAL) + if vMode is not None: + swingModes.append(SWING_VERTICAL) + if hMode is not None: + swingModes.append(SWING_BOTH) + return swingModes + + async def async_set_swing_mode(self, mode): + """Set the preset mode status.""" + hMode = self.getValue(ATTR_HSWING_MODE) + vMode = self.getValue(ATTR_VSWING_MODE) + new_hMode = ( + ATTR_SWING_SWING + if mode == SWING_HORIZONTAL or mode == SWING_BOTH + else ATTR_SWING_STOP + ) + new_vMode = ( + ATTR_SWING_SWING + if mode == SWING_VERTICAL or mode == SWING_BOTH + else ATTR_SWING_STOP + ) + if hMode != new_hMode: + await self.setValue(ATTR_HSWING_MODE, new_hMode) + if vMode != new_vMode: + await self.setValue(ATTR_VSWING_MODE, new_vMode) + + @property + def support_humidity(self): + """Return True if the device has humidity sensor.""" + return False + + @property + def support_outside_temperature(self): + """Return True if the device supports outsite temperature measurement.""" + return self.getData(ATTR_OUTSIDE_TEMPERATURE) is not None + + @property + def outside_temperature(self): + """Return current outside temperature.""" + return float(self.getValue(ATTR_OUTSIDE_TEMPERATURE)) + + @property + def inside_temperature(self): + """Return current inside temperature.""" + return float(self.getValue(ATTR_INSIDE_TEMPERATURE)) + + @property + def target_temperature(self): + """Return current target temperature.""" + operationMode = self.getValue(ATTR_OPERATION_MODE) + if operationMode not in ["auto", "cooling", "heating"]: + return None + + return float(self.getValue(ATTR_TARGET_TEMPERATURE)) + + @property + def target_temperature_step(self): + """Return current target temperature.""" + operationMode = self.getValue(ATTR_OPERATION_MODE) + if operationMode not in ["auto", "cooling", "heating"]: + return None + return float(self.getData(ATTR_TARGET_TEMPERATURE)["stepValue"]) + + async def async_set_temperature(self, value): + """Set new target temperature.""" + operationMode = self.getValue(ATTR_OPERATION_MODE) + if operationMode not in ["auto", "cooling", "heating"]: + return None + return await self.setValue(ATTR_TARGET_TEMPERATURE, value) + + @property + def support_energy_consumption(self): + """Return True if the device supports energy consumption monitoring.""" + return self.getData(ATTR_OUTSIDE_TEMPERATURE) is not None + + def energy_consumption(self, mode, period): + """Return the last hour cool power consumption of a given mode in kWh.""" + energy_data = [ + 0 if v is None else v + for v in self.getData(ATTR_ENERGY_CONSUMPTION)[mode][period] + ] + start_index = 7 if period == SENSOR_PERIOD_WEEKLY else 12 + return sum(energy_data[start_index:]) + + async def set(self, settings): + """Set settings on Daikin device.""" + raise NotImplementedError diff --git a/custom_components/daikin_residential/device.py b/custom_components/daikin_residential/device.py new file mode 100644 index 0000000..5d33891 --- /dev/null +++ b/custom_components/daikin_residential/device.py @@ -0,0 +1,358 @@ +import datetime +import logging +import json + +from homeassistant.util import Throttle + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from .const import DOMAIN + + +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=15) + + +_LOGGER = logging.getLogger(__name__) + + +class DaikinResidentialDevice: + """Class to represent and control one Daikin Residential Device.""" + + def __init__(self, jsonData, apiInstance): + """Initialize a new Daikin Residential Device.""" + self.api = apiInstance + + self.setJsonData(jsonData) + self.name = self.get_value("climateControl", "name") + # self.ip_address = device.device_ip + self._available = True + _LOGGER.info( + "Initialized Daikin Residential Device '%s' (id %s)", + self.name, + self.getId(), + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + def device_info(self): + """Return a device description for device registry.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.getId()) + }, + "connections": { + (CONNECTION_NETWORK_MAC, self.get_value("gateway", "macAddress")) + }, + "manufacturer": "Daikin", + "model": self.get_value("gateway", "modelInfo"), + "name": self.get_value("climateControl", "name"), + "sw_version": self.get_value("gateway", "firmwareVersion").replace( + "_", "." + ), + } + + """ + * Helper method to traverse the Device object returned + * by Daikin cloud for subPath datapoints + * + * @param {object} obj Object to traverse + * @param {object} data Data object where all data are collected + * @param {string} [pathPrefix] remember the path when traversing through structure + * @returns {object} collected data + """ + + def _traverseDatapointStructure(self, obj, data={}, pathPrefix=""): + """Helper method to traverse the Device object returned + by Daikin cloud for subPath datapoints.""" + for key in obj.keys(): + subKeys = obj[key].keys() + if ( + key == "meta" + or "value" in subKeys + or "settable" in subKeys + or "unit" in subKeys + ): + # we found end leaf + # print('FINAL ' + pathPrefix + '/' + key) + data[pathPrefix + "/" + key] = obj[key] + elif type(obj[key]) == dict: + # go one level deeper + # print(' found ' + key) + self._traverseDatapointStructure(obj[key], data, pathPrefix + "/" + key) + else: + _LOGGER.error("SOMETHING IS WRONG WITH KEY %s", key) + return data + + def setJsonData(self, desc): + """Set a device description and parse/traverse data structure.""" + self.desc = desc + # re-map some data for more easy access + self.managementPoints = {} + dataPoints = {} + + for mp in self.desc["managementPoints"]: + # print('AAAA: [{}] [{}]'.format(mp['embeddedId'], mp)) + dataPoints = {} + for key in mp.keys(): + dataPoints[key] = {} + if mp[key] is None: + continue + if type(mp[key]) != dict: + continue + if type(mp[key]["value"]) != dict or ( + len(mp[key]["value"]) == 1 and "enabled" in mp[key]["value"] + ): + dataPoints[key] = mp[key] + else: + # print('TRAVERSE ' + key + ': ' + json.dumps(dataPoints[key])); + dataPoints[key] = self._traverseDatapointStructure( + mp[key]["value"], {} + ) + + self.managementPoints[mp["embeddedId"]] = dataPoints + + # print('MPS FOUND: [{}]'.format(self.managementPoints)) + # print('MPS FOUND: [{}]'.format(self.managementPoints.keys())) + + def getId(self): + """Get Daikin Device UUID.""" + return self.desc["id"] + + def getDescription(self): + """Get the original Daikin Device Description.""" + return self.desc + + def getLastUpdated(self): + """Get the timestamp when data were last updated.""" + return self.desc["lastUpdateReceived"] + # return new Date(self.desc.lastUpdateReceived) + + """ + * Get a current data object (includes value and meta information). + * Without any parameter the full internal data structure is returned and + * can be further detailed by sending parameters + * + * @param {string} [managementPoint] Management point name + * @param {string} [dataPoint] Datapoint name for management point + * @param {string} [dataPointPath] further detailed datapoints with subpath data + * @returns {object|null} Data object + """ + + def get_data(self, managementPoint=None, dataPoint=None, dataPointPath=""): + """Get a current data object (includes value and meta information).""" + if managementPoint is None: + # return all data + return self.managementPoints + + if managementPoint not in self.managementPoints: + return None + + if dataPoint is None: + # return data from one managementPoint + return self.managementPoints[managementPoint] + + if dataPoint not in self.managementPoints[managementPoint]: + return None + + if dataPointPath == "": + # return data from one managementPoint and dataPoint + return self.managementPoints[managementPoint][dataPoint] + + return self.managementPoints[managementPoint][dataPoint][dataPointPath] + + def get_value(self, managementPoint=None, dataPoint=None, dataPointPath=""): + """Get the current value of a data object.""" + return self.get_data(managementPoint, dataPoint, dataPointPath)["value"] + + def get_valid_values(self, managementPoint=None, dataPoint=None, dataPointPath=""): + """Get a list of the accepted values of a data object.""" + return self.get_data(managementPoint, dataPoint, dataPointPath)["values"] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def updateData(self): + """Update the data of self device from the cloud.""" + return + # TODO: Enhance self method to also allow to get some partial data + # like only one managementPoint or such; needs checking how to request + print("DEV UPDATE " + self.name) + desc = await self.api.doBearerRequest("/v1/gateway-devices/" + self.getId()) + self.setJsonData(desc) + print("DEVICE: " + self.name) + print( + " temp: inner " + + str(self.get_value("climateControl", "sensoryData", "/roomTemperature")) + + " outer " + + str( + self.get_value("climateControl", "sensoryData", "/outdoorTemperature") + ) + ) + print( + " current mode: " + + str(self.get_value("climateControl", "operationMode")) + + " " + + str(self.get_value("climateControl", "onOffMode")) + ) + print( + " target temp: " + + str( + self.get_value( + "climateControl", + "temperatureControl", + "/operationModes/cooling/setpoints/roomTemperature", + ) + ) + ) + print( + " FAN: mode [{}] speed [{}]\n".format( + self.get_value( + "climateControl", + "fanControl", + "/operationModes/auto/fanSpeed/currentMode", + ), + self.get_value( + "climateControl", + "fanControl", + "/operationModes/auto/fanSpeed/modes/fixed", + ), + ) + ) + return True + + def _validateData(self, dataPoint, descr, value): + """Validates a value that should be sent to the Daikin Device.""" + + if "value" not in descr: # and not 'settable' in descr: + raise Exception("Value can not be set without dataPointPath") + + if "settable" not in descr or not descr["settable"]: + raise Exception("Data point " + dataPoint + " is not writable") + + if "stepValue" in descr and type(descr["stepValue"]) != type(value): + raise Exception( + "Type of value (" + + str(type(value)) + + ") is not the expected type (" + + str(type(descr["value"])) + + ")" + ) + + if ( + "values" in descr + and isinstance(descr["values"], list) + and value not in descr["values"] + ): + raise Exception( + "Value (" + + str(value) + + ") is not in the list of allowed values: " + + "/".join(map(str, descr["values"])) + ) + + if ( + "maxLength" in descr + and type(descr["maxLength"]) == int + and type(value) == str + and len(value) > descr["maxLength"] + ): + raise Exception( + "Length of value (" + + str(len(value)) + + ") is greater than the allowed " + + str(descr["maxLength"]) + + " characters" + ) + + if ( + "minValue" in descr + and type(descr["minValue"]) == int + and type(value) in (int, float) + and float(value) < descr["minValue"] + ): + raise Exception( + "Value (" + + str(value) + + ") must not be smaller than " + + str(descr["minValue"]) + ) + + if ( + "maxValue" in descr + and type(descr["maxValue"]) == int + and type(value) in (int, float) + and float(value) > descr["maxValue"] + ): + raise Exception( + "Value (" + + str(value) + + ") must not be bigger than " + + str(descr["maxValue"]) + ) + + # TODO add more validations for stepValue(number) + + """ + * Set a datapoint on this device + * + * @param {string} managementPoint Management point name + * @param {string} dataPoint Datapoint name for management point + * @param {string} [dataPointPath] further detailed datapoints with subpath data + * @param {number|string} value Value to set + * @returns {Promise} should return a true - or if a body + * is returned the body object (can this happen?) + """ + + async def set_data(self, managementPoint, dataPoint, dataPointPath="", value=None): + """Set a datapoint on this device.""" + if value is None: + value = dataPointPath + dataPointPath = "" + + if dataPoint not in self.managementPoints[managementPoint].keys() or ( + dataPointPath != "" + and dataPointPath not + in self.managementPoints[managementPoint][dataPoint].keys() + ): + raise Exception( + "Please provide a valid datapoint definition " + "that exists in the data structure" + ) + + dataPointDef = ( + self.managementPoints[managementPoint][dataPoint][dataPointPath] + if dataPointPath != "" + else self.managementPoints[managementPoint][dataPoint] + ) + _LOGGER.debug( + "Trying to validate " + str(value) + " with description: %s", + format(dataPointDef), + ) + try: + self._validateData(dataPoint + dataPointPath, dataPointDef, value) + except Exception as error: + print(error) + _LOGGER.error("FAILED to validate set_data params: %s", format(error)) + return + + setPath = ( + "/v1/gateway-devices/" + + self.getId() + + "/management-points/" + + managementPoint + + "/characteristics/" + + dataPoint + ) + setBody = {"value": value} + if dataPointPath != "": + setBody["path"] = dataPointPath + setOptions = {"method": "PATCH", "json": json.dumps(setBody)} + + _LOGGER.debug("Path: " + setPath + " , options: %s", setOptions) + + self.api._delay_next_request = True + res = await self.api.doBearerRequest(setPath, setOptions) + _LOGGER.debug("RES IS {}".format(res)) + if res is True: + self.get_data(managementPoint, dataPoint, dataPointPath)["value"] = value diff --git a/custom_components/daikin_residential/manifest.json b/custom_components/daikin_residential/manifest.json new file mode 100644 index 0000000..9c9ff86 --- /dev/null +++ b/custom_components/daikin_residential/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "daikin_residential", + "name": "Daikin Residential Controller", + "version": "1.0.0", + "documentation": "https://github.com/rospogrigio/daikin_residential/", + "dependencies": [], + "codeowners": [ + "@rospogrigio" + ], + "issue_tracker": "https://github.com/rospogrigio/daikin_residential/issues", + "requirements": [], + "iot_class": "cloud_polling", + "config_flow": true +} diff --git a/custom_components/daikin_residential/sensor.py b/custom_components/daikin_residential/sensor.py new file mode 100644 index 0000000..faad256 --- /dev/null +++ b/custom_components/daikin_residential/sensor.py @@ -0,0 +1,148 @@ +"""Support for Daikin AC sensors.""" +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.helpers.entity import Entity + +from .daikin_base import Appliance + +from .const import ( + DOMAIN as DAIKIN_DOMAIN, + DAIKIN_DEVICES, + ATTR_COOL_ENERGY, + ATTR_HEAT_ENERGY, + ATTR_INSIDE_TEMPERATURE, + ATTR_OUTSIDE_TEMPERATURE, + SENSOR_TYPE_ENERGY, + SENSOR_TYPE_HUMIDITY, + SENSOR_TYPE_POWER, + SENSOR_TYPE_TEMPERATURE, + SENSOR_PERIODS, + SENSOR_TYPES, +) + + +async def async_setup(hass, async_add_entities): + """Old way of setting up the Daikin sensors. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Daikin climate based on config_entry.""" + sensors = [] + for dev_id, device in hass.data[DAIKIN_DOMAIN][DAIKIN_DEVICES].items(): + sensor = DaikinSensor.factory(device, ATTR_INSIDE_TEMPERATURE) + sensors.append(sensor) + + if device.support_outside_temperature: + sensor = DaikinSensor.factory(device, ATTR_OUTSIDE_TEMPERATURE) + sensors.append(sensor) + if device.support_energy_consumption: + for period in SENSOR_PERIODS: + sensor = DaikinSensor.factory(device, ATTR_COOL_ENERGY, period) + sensors.append(sensor) + sensor = DaikinSensor.factory(device, ATTR_HEAT_ENERGY, period) + sensors.append(sensor) + async_add_entities(sensors) + + +class DaikinSensor(Entity): + """Representation of a Sensor.""" + + @staticmethod + def factory(device: Appliance, monitored_state: str, period=""): + """Initialize any DaikinSensor.""" + cls = { + SENSOR_TYPE_TEMPERATURE: DaikinClimateSensor, + SENSOR_TYPE_HUMIDITY: DaikinClimateSensor, + SENSOR_TYPE_POWER: DaikinPowerSensor, + SENSOR_TYPE_ENERGY: DaikinPowerSensor, + }[SENSOR_TYPES[monitored_state][CONF_TYPE]] + return cls(device, monitored_state, period) + + def __init__(self, device: Appliance, monitored_state: str, period="") -> None: + """Initialize the sensor.""" + self._device = device + self._sensor = SENSOR_TYPES[monitored_state] + self._period = period + if period != "": + periodName = SENSOR_PERIODS[period] + self._name = f"{device.name} {periodName} {self._sensor[CONF_NAME]}" + else: + self._name = f"{device.name} {self._sensor[CONF_NAME]}" + self._device_attribute = monitored_state + + @property + def unique_id(self): + """Return a unique ID.""" + devID = self._device.getId() + if self._period != "": + return f"{devID}_{self._device_attribute}_{self._period}" + return f"{devID}_{self._device_attribute}" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + raise NotImplementedError + + @property + def device_class(self): + """Return the class of this device.""" + return self._sensor.get(CONF_DEVICE_CLASS) + + @property + def icon(self): + """Return the icon of this device.""" + return self._sensor.get(CONF_ICON) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._sensor[CONF_UNIT_OF_MEASUREMENT] + + async def async_update(self): + """Retrieve latest state.""" + await self._device.api.async_update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._device.device_info() + + +class DaikinClimateSensor(DaikinSensor): + """Representation of a Climate Sensor.""" + + @property + def state(self): + """Return the internal state of the sensor.""" + if self._device_attribute == ATTR_INSIDE_TEMPERATURE: + return self._device.inside_temperature + if self._device_attribute == ATTR_OUTSIDE_TEMPERATURE: + return self._device.outside_temperature + return None + + +class DaikinPowerSensor(DaikinSensor): + """Representation of a power/energy consumption sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self._device_attribute == ATTR_COOL_ENERGY: + return round(self._device.energy_consumption("cooling", self._period), 3) + if self._device_attribute == ATTR_HEAT_ENERGY: + return round(self._device.energy_consumption("heating", self._period), 3) + return None diff --git a/custom_components/daikin_residential/strings.json b/custom_components/daikin_residential/strings.json new file mode 100644 index 0000000..6aac6d6 --- /dev/null +++ b/custom_components/daikin_residential/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Daikin Residential Controller AC", + "description": "No configuration parameters are needed in this menu.\n\nJust make sure that you have obtained your tokenset.json file using the MITM proxy procedure, and stored it in your configuration folder, then press Submit to proceed.", + "data": { + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "missing_config": "[%key:common::config_flow::abort::missing_config%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/custom_components/daikin_residential/switch.py b/custom_components/daikin_residential/switch.py new file mode 100644 index 0000000..93acd4d --- /dev/null +++ b/custom_components/daikin_residential/switch.py @@ -0,0 +1,78 @@ +"""Support for Daikin AirBase zones.""" +from homeassistant.helpers.entity import ToggleEntity + +from .daikin_base import Appliance + +from .const import ( + DOMAIN as DAIKIN_DOMAIN, + DAIKIN_DEVICES, + ATTR_STATE_OFF, + ATTR_STATE_ON, + PRESET_STREAMER, +) + +SWITCH_ICON = "hass:air-filter" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Daikin climate based on config_entry.""" + for dev_id, device in hass.data[DAIKIN_DOMAIN][DAIKIN_DEVICES].items(): + switches = [PRESET_STREAMER] + if device.support_preset_mode(PRESET_STREAMER): + async_add_entities([DaikinSwitch(device, switch) for switch in switches]) + + +class DaikinSwitch(ToggleEntity): + """Representation of a switch.""" + + def __init__(self, device: Appliance, switch_id: str): + """Initialize the zone.""" + self._device = device + self._switch_id = switch_id + self._name = f"{device.name} {switch_id}" + + @property + def unique_id(self): + """Return a unique ID.""" + devID = self._device.getId() + return f"{devID}-{self._switch_id}" + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SWITCH_ICON + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the state of the switch.""" + return self._device.preset_mode_status(self._switch_id) == ATTR_STATE_ON + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._device.device_info() + + async def async_update(self): + """Retrieve latest state.""" + await self._device.api.async_update() + + async def async_turn_on(self, **kwargs): + """Turn the zone on.""" + await self._device.set_preset_mode_status(self._switch_id, ATTR_STATE_ON) + + async def async_turn_off(self, **kwargs): + """Turn the zone off.""" + await self._device.set_preset_mode_status(self._switch_id, ATTR_STATE_OFF) diff --git a/custom_components/daikin_residential/translations/bg.json b/custom_components/daikin_residential/translations/bg.json new file mode 100644 index 0000000..a1f8209 --- /dev/null +++ b/custom_components/daikin_residential/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/ca.json b/custom_components/daikin_residential/translations/ca.json new file mode 100644 index 0000000..eecb002 --- /dev/null +++ b/custom_components/daikin_residential/translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "device_fail": "Error inesperat", + "device_timeout": "Ha fallat la connexi\u00f3", + "forbidden": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "host": "Amfitri\u00f3", + "key": "Clau API", + "password": "Contrasenya" + }, + "description": "Introdueix l'Adre\u00e7a IP del teu AC Daikin.\n\nTingues en compte que la Clau API i la Contrasenya s'utilitzen nom\u00e9s als dispositius BRP072Cxx i SKYFi respectivament.", + "title": "Configuraci\u00f3 de Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/cs.json b/custom_components/daikin_residential/translations/cs.json new file mode 100644 index 0000000..2fe8f63 --- /dev/null +++ b/custom_components/daikin_residential/translations/cs.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "device_fail": "Neo\u010dek\u00e1van\u00e1 chyba", + "device_timeout": "Nepoda\u0159ilo se p\u0159ipojit", + "forbidden": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "Hostitel", + "key": "Kl\u00ed\u010d API", + "password": "Heslo" + }, + "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\nV\u0161imn\u011bte si, \u017ee Kl\u00ed\u010d API a Heslo jsou pou\u017eit\u00e9 za\u0159\u00edzen\u00edm BRP072Cxx, respektive SKYFi.", + "title": "Nastaven\u00ed Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/da.json b/custom_components/daikin_residential/translations/da.json new file mode 100644 index 0000000..6502ced --- /dev/null +++ b/custom_components/daikin_residential/translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt" + }, + "description": "Indtast IP-adresse p\u00e5 dit Daikin AC.", + "title": "Konfigurer Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/de.json b/custom_components/daikin_residential/translations/de.json new file mode 100644 index 0000000..cebe5f8 --- /dev/null +++ b/custom_components/daikin_residential/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "device_timeout": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "key": "Authentifizierungsschl\u00fcssel (wird nur von BRP072C / Zena-Ger\u00e4ten verwendet)", + "password": "Passwort" + }, + "description": "Gib die IP-Adresse deiner Daikin AC ein.", + "title": "Daikin AC konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/en.json b/custom_components/daikin_residential/translations/en.json new file mode 100644 index 0000000..282e470 --- /dev/null +++ b/custom_components/daikin_residential/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "The integration is already configured.", + "cannot_connect": "Failed to connect", + "missing_config": "tokenset.json file not found in config folder." + }, + "error": { + "cannot_connect": "Failed to connect", + "device_fail": "Unexpected error", + "device_timeout": "Failed to connect", + "forbidden": "Invalid authentication", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + }, + "description": "No configuration parameters are needed in this menu.\n\nJust make sure that you have obtained your tokenset.json file using the MITM proxy procedure, and stored it in your configuration folder, then press Submit to proceed.", + "title": "Configure Daikin Residential Controller AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/es-419.json b/custom_components/daikin_residential/translations/es-419.json new file mode 100644 index 0000000..8667011 --- /dev/null +++ b/custom_components/daikin_residential/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Introduzca la direcci\u00f3n IP de su Daikin AC.", + "title": "Configurar Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/es.json b/custom_components/daikin_residential/translations/es.json new file mode 100644 index 0000000..7f690b0 --- /dev/null +++ b/custom_components/daikin_residential/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "device_fail": "Error inesperado", + "device_timeout": "No se pudo conectar", + "forbidden": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "host": "Host", + "key": "Clave API", + "password": "Contrase\u00f1a" + }, + "description": "Introduce la direcci\u00f3n IP de tu aire acondicionado Daikin.\n\nTen en cuenta que la Clave API y la Contrase\u00f1a son usadas por los dispositivos BRP072Cxx y SKYFi respectivamente.", + "title": "Configurar aire acondicionado Daikin" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/et.json b/custom_components/daikin_residential/translations/et.json new file mode 100644 index 0000000..85e69c3 --- /dev/null +++ b/custom_components/daikin_residential/translations/et.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "device_fail": "Tundmatu viga", + "device_timeout": "\u00dchendamine nurjus", + "forbidden": "Tuvastamine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "host": "", + "key": "API v\u00f5ti", + "password": "Salas\u00f5na" + }, + "description": "Sisesta oma Daikin AC IP aadress.\nPane t\u00e4hele, et API v\u00f5ti ja Salas\u00f5na kasutavad ainult seadmed BRP072Cxx ja SKYFi.", + "title": "Seadista Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/fi.json b/custom_components/daikin_residential/translations/fi.json new file mode 100644 index 0000000..ed772ef --- /dev/null +++ b/custom_components/daikin_residential/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Palvelin" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/fr.json b/custom_components/daikin_residential/translations/fr.json new file mode 100644 index 0000000..75f3260 --- /dev/null +++ b/custom_components/daikin_residential/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "device_fail": "Erreur inattendue", + "device_timeout": "Echec de la connexion", + "forbidden": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP", + "key": "Cl\u00e9 d'authentification (utilis\u00e9e uniquement par les appareils BRP072C/Zena)", + "password": "Mot de passe de l'appareil (utilis\u00e9 uniquement par les appareils SKYFi)" + }, + "description": "Saisissez l'adresse IP de votre Daikin AC. \n\n Notez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les p\u00e9riph\u00e9riques BRP072Cxx et SKYFi.", + "title": "Configurer Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/he.json b/custom_components/daikin_residential/translations/he.json new file mode 100644 index 0000000..bde1115 --- /dev/null +++ b/custom_components/daikin_residential/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "device_fail": "\u05d0\u05d9\u05e8\u05d0\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4", + "device_timeout": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "forbidden": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "key": "\u05de\u05e4\u05ea\u05d7 API", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/hu.json b/custom_components/daikin_residential/translations/hu.json new file mode 100644 index 0000000..b11dba9 --- /dev/null +++ b/custom_components/daikin_residential/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "device_fail": "V\u00e1ratlan hiba", + "device_timeout": "Sikertelen csatlakoz\u00e1s", + "forbidden": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "key": "API kulcs", + "password": "Jelsz\u00f3" + }, + "description": "Add meg a Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 IP-c\u00edm\u00e9t.", + "title": "A Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/it.json b/custom_components/daikin_residential/translations/it.json new file mode 100644 index 0000000..edcd567 --- /dev/null +++ b/custom_components/daikin_residential/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata.", + "cannot_connect": "Impossibile connettersi", + "missing_config": "File tokenset.json non trovato nella directory di configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "device_fail": "Errore imprevisto", + "device_timeout": "Impossibile connettersi", + "forbidden": "Autenticazione non valida", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "host": "Host", + "key": "Chiave API", + "password": "Password" + }, + "description": "Non \u00e8 necessario introdurre parametri di configurazione in questo mennu.\n\nAssicurati solo di aver ottenuto il tuo file tokenset.json usando la procedura con il MITM proxy, e di averlo salvato nella tua directory di configurazione, poi premi Submit per procedere.", + "title": "Configura Daikin Residential Controller AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/ko.json b/custom_components/daikin_residential/translations/ko.json new file mode 100644 index 0000000..e5981dc --- /dev/null +++ b/custom_components/daikin_residential/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "device_fail": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "device_timeout": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "forbidden": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "key": "API \ud0a4", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud558\uc138\uc694.", + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/lb.json b/custom_components/daikin_residential/translations/lb.json new file mode 100644 index 0000000..aa59b6d --- /dev/null +++ b/custom_components/daikin_residential/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen" + }, + "error": { + "device_fail": "Onerwaarte Feeler", + "device_timeout": "Feeler beim verbannen", + "forbidden": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "key": "API Schl\u00ebssel", + "password": "Passwuert" + }, + "description": "Gitt d'IP Adresse vum Daikin AC an.\n\nRemarque: API Schl\u00ebssel a Passwuert gi vu BRP072Cxx a SKYFi Apparater respektiv benotzt.", + "title": "Daikin AC konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/nl.json b/custom_components/daikin_residential/translations/nl.json new file mode 100644 index 0000000..775e358 --- /dev/null +++ b/custom_components/daikin_residential/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + }, + "description": "Voer het IP-adres van uw Daikin AC in.", + "title": "Daikin AC instellen" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/no.json b/custom_components/daikin_residential/translations/no.json new file mode 100644 index 0000000..6bcf2b2 --- /dev/null +++ b/custom_components/daikin_residential/translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "device_fail": "Uventet feil", + "device_timeout": "Tilkobling mislyktes", + "forbidden": "Ugyldig godkjenning", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "Vert", + "key": "API-n\u00f8kkel", + "password": "Passord" + }, + "description": "Skriv inn IP adresse av Daikin AC.\n\n Merk at API-n\u00f8kkel og Passord bare brukes av henholdsvis BRP072Cxx og SKYFi-enheter.", + "title": "Konfigurer Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/pl.json b/custom_components/daikin_residential/translations/pl.json new file mode 100644 index 0000000..f20666f --- /dev/null +++ b/custom_components/daikin_residential/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "device_fail": "Nieoczekiwany b\u0142\u0105d", + "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "forbidden": "Niepoprawne uwierzytelnienie", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "host": "Nazwa hosta lub adres IP", + "key": "Klucz API", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a adres IP klimatyzacji Daikin.\n\nZwr\u00f3\u0107 uwag\u0119, \u017ce klucz API oraz has\u0142o u\u017cywane s\u0105 odpowiednio tylko przez urz\u0105dzenia BRP072Cxx i SKYFi.", + "title": "Konfiguracja Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/pt-BR.json b/custom_components/daikin_residential/translations/pt-BR.json new file mode 100644 index 0000000..a844969 --- /dev/null +++ b/custom_components/daikin_residential/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na conex\u00e3o" + }, + "error": { + "device_fail": "Erro inesperado", + "device_timeout": "Falha ao conectar", + "forbidden": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Digite o endere\u00e7o IP do seu AC Daikin.", + "title": "Configurar o AC Daikin" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/pt.json b/custom_components/daikin_residential/translations/pt.json new file mode 100644 index 0000000..617aed2 --- /dev/null +++ b/custom_components/daikin_residential/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + }, + "description": "Introduza o endere\u00e7o IP do seu Daikin AC.", + "title": "Configurar o Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/ru.json b/custom_components/daikin_residential/translations/ru.json new file mode 100644 index 0000000..8587232 --- /dev/null +++ b/custom_components/daikin_residential/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "device_timeout": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "forbidden": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "key": "\u041a\u043b\u044e\u0447 API", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Daikin AC. \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u041a\u043b\u044e\u0447 API \u0438 \u041f\u0430\u0440\u043e\u043b\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 BRP072Cxx \u0438 SKYFi \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e.", + "title": "Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/sl.json b/custom_components/daikin_residential/translations/sl.json new file mode 100644 index 0000000..a9f8514 --- /dev/null +++ b/custom_components/daikin_residential/translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj" + }, + "description": "Vnesite naslov IP va\u0161e Daikin klime.", + "title": "Nastavite Daikin klimo" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/sv.json b/custom_components/daikin_residential/translations/sv.json new file mode 100644 index 0000000..8c3e221 --- /dev/null +++ b/custom_components/daikin_residential/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rddatorn", + "key": "API nyckel", + "password": "Enhetsl\u00f6senord (anv\u00e4nds endast av SKYFi-enheter)" + }, + "description": "Ange IP-adressen f\u00f6r din Daikin AC.", + "title": "Konfigurera Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/th.json b/custom_components/daikin_residential/translations/th.json new file mode 100644 index 0000000..4d01bb3 --- /dev/null +++ b/custom_components/daikin_residential/translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0e42\u0e2e\u0e2a\u0e15\u0e4c" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/zh-Hans.json b/custom_components/daikin_residential/translations/zh-Hans.json new file mode 100644 index 0000000..0acb511 --- /dev/null +++ b/custom_components/daikin_residential/translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u7801\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "api_key": "API\u5bc6\u7801", + "host": "\u4e3b\u673a" + }, + "description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684 IP \u5730\u5740\u3002", + "title": "\u914d\u7f6e Daikin \u7a7a\u8c03" + } + } + } +} \ No newline at end of file diff --git a/custom_components/daikin_residential/translations/zh-Hant.json b/custom_components/daikin_residential/translations/zh-Hant.json new file mode 100644 index 0000000..6ebd03b --- /dev/null +++ b/custom_components/daikin_residential/translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "device_fail": "\u672a\u9810\u671f\u932f\u8aa4", + "device_timeout": "\u9023\u7dda\u5931\u6557", + "forbidden": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "host": "\u4e3b\u6a5f\u7aef", + "key": "API \u5bc6\u9470", + "password": "\u5bc6\u78bc" + }, + "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u8a2d\u5099\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", + "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" + } + } + } +} \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..c861f53 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,8 @@ +black==20.8b1 +codespell==2.0.0 +flake8==3.8.4 +mypy==0.800 +pydocstyle==5.1.1 +pylint==2.6.0 +pylint-strict-informational==0.1 +homeassistant==2021.1.4 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2d94fd3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[flake8] +exclude = .git,.tox +max-line-length = 88 +ignore = E203, W503 + +[mypy] +python_version = 3.7 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true + +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +strict = true +ignore_errors = false +warn_unreachable = true +# TODO: turn these off, address issues +allow_any_generics = true +implicit_reexport = true diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4796192 --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] +skipsdist = true +envlist = py{37,38}, lint, typing +skip_missing_interpreters = True +cs_exclude_words = hass,unvalid + +[gh-actions] +python = + 3.7: clean, py37, lint, typing + 3.8: clean, py38, lint, typing + +[testenv] +passenv = TOXENV CI +whitelist_externals = + true +setenv = + LANG=en_US.UTF-8 + PYTHONPATH = {toxinidir}/daikin_residential-homeassistant +deps = + -r{toxinidir}/requirements_test.txt +commands = + true # TODO: Run tests later + #pytest -n auto --log-level=debug -v --timeout=30 --durations=10 {posargs} + +[testenv:lint] +ignore_errors = True +deps = + {[testenv]deps} +commands = + codespell -q 4 -L {[tox]cs_exclude_words} --skip="*.pyc,*.pyi,*~" custom_components + flake8 custom_components + black --fast --check . + pydocstyle -v custom_components + pylint custom_components/daikin_residential + +[testenv:typing] +commands = + mypy --ignore-missing-imports --follow-imports=skip custom_components