diff --git a/custom_components/panasonic_cc/climate.py b/custom_components/panasonic_cc/climate.py index 4ca686f..252ba1b 100644 --- a/custom_components/panasonic_cc/climate.py +++ b/custom_components/panasonic_cc/climate.py @@ -10,6 +10,7 @@ from homeassistant.const import UnitOfTemperature from . import PANASONIC_DEVICES +from .panasonic import PanasonicApiDevice from .const import ( SUPPORT_FLAGS, @@ -60,7 +61,7 @@ async def async_setup_entry(hass, entry, async_add_entities): class PanasonicClimateDevice(ClimateEntity): - def __init__(self, api): + def __init__(self, api: PanasonicApiDevice): """Initialize the climate device.""" self._api = api self._attr_hvac_action = HVACAction.IDLE @@ -207,11 +208,7 @@ def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp. Requires SUPPORT_PRESET_MODE. """ - eco = self._api.eco_mode - for key, value in PRESET_LIST.items(): - if value == eco: - #_LOGGER.debug("Preset mode is {0}".format(key)) - return key + return self._api.preset_mode async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" @@ -223,17 +220,17 @@ def preset_modes(self) -> Optional[List[str]]: Requires SUPPORT_PRESET_MODE. """ #_LOGGER.debug("Preset modes are {0}".format(",".join(PRESET_LIST.keys()))) - return list(PRESET_LIST.keys()) + return self._api.available_presets @property def min_temp(self): """Return the minimum temperature.""" - return 16 + return self._api.min_temp @property def max_temp(self): """Return the maximum temperature.""" - return 30 + return self._api.max_temp @property def target_temp_step(self): diff --git a/custom_components/panasonic_cc/const.py b/custom_components/panasonic_cc/const.py index 0804c89..4da0b92 100644 --- a/custom_components/panasonic_cc/const.py +++ b/custom_components/panasonic_cc/const.py @@ -29,6 +29,8 @@ SENSOR_TYPE_TEMPERATURE = "temperature" +PRESET_8_15 = "+8/15" + SENSOR_TYPES = { ATTR_INSIDE_TEMPERATURE: { CONF_NAME: "Inside Temperature", @@ -67,7 +69,8 @@ PRESET_LIST = { PRESET_NONE: 'Auto', PRESET_BOOST: 'Powerful', - PRESET_ECO: 'Quiet' + PRESET_ECO: 'Quiet', + PRESET_8_15: '+8/15' } OPERATION_LIST = { diff --git a/custom_components/panasonic_cc/manifest.json b/custom_components/panasonic_cc/manifest.json index 66b0eca..9023640 100644 --- a/custom_components/panasonic_cc/manifest.json +++ b/custom_components/panasonic_cc/manifest.json @@ -2,7 +2,7 @@ "domain": "panasonic_cc", "name": "Panasonic Comfort Cloud", "after_dependencies": ["http"], - "version": "1.0.48", + "version": "1.0.49", "config_flow": true, "documentation": "https://github.com/sockless-coding/panasonic_cc/", "dependencies": [], diff --git a/custom_components/panasonic_cc/panasonic.py b/custom_components/panasonic_cc/panasonic.py index 530627d..fa4e4ac 100644 --- a/custom_components/panasonic_cc/panasonic.py +++ b/custom_components/panasonic_cc/panasonic.py @@ -2,15 +2,19 @@ from datetime import timedelta import logging from datetime import datetime +from functools import cached_property from typing import Optional from homeassistant.util import Throttle from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.components.climate.const import ATTR_HVAC_MODE +from homeassistant.helpers.storage import Store from .pcomfortcloud.apiclient import ApiClient +from .pcomfortcloud.panasonicdevice import PanasonicDevice +from .pcomfortcloud import constants -from .const import PRESET_LIST, OPERATION_LIST +from .const import PRESET_LIST, OPERATION_LIST, PRESET_8_15, PRESET_NONE, PRESET_ECO, PRESET_BOOST _LOGGER = logging.getLogger(__name__) @@ -25,6 +29,7 @@ def __init__(self, hass: HomeAssistant, api: ApiClient, device, force_outside_se self.hass = hass self._api = api self.device = device + self._details: PanasonicDevice = None self.force_outside_sensor = force_outside_sensor self.enable_energy_sensor = enable_energy_sensor self.id = device['id'] @@ -38,6 +43,8 @@ def __init__(self, hass: HomeAssistant, api: ApiClient, device, force_outside_se self.current_power_counter = 0 self._available = True self.constants = constants + self._store = Store(hass, version=1, key=f"panasonic_cc_{self.id}") + self._is_on = False self._inside_temperature = None @@ -53,8 +60,6 @@ def __init__(self, hass: HomeAssistant, api: ApiClient, device, force_outside_se self.features = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) async def update(self, **kwargs): await self.do_update() @@ -83,28 +88,23 @@ async def do_update(self): self._available = False _LOGGER.debug("Received no data for device {id}".format(**self.device)) return + self._details = data try: _LOGGER.debug("Data: {}".format(data)) - if self.features is None: - self.features = data.get('features', None) - - plst = data.get('parameters') - self._is_on = bool(plst.get('power').value) - if plst.get('temperatureInside') != 126: - self._inside_temperature = plst.get('temperatureInside') - if plst.get('temperatureOutside') != 126: - self._outside_temperature = plst.get('temperatureOutside') - if self._inside_temperature is None and self._outside_temperature is not None: - self._inside_temperature = self._outside_temperature - if plst.get('temperature') != 126: - self._target_temperature = plst.get('temperature') - self._fan_mode = plst.get('fanSpeed').name - self._swing_mode = plst.get('airSwingVertical').name - self._swing_lr_mode = plst.get('airSwingHorizontal').name - self._hvac_mode = plst.get('mode').name - self._eco_mode = plst.get('eco').name - self._nanoe_mode = plst.get('nanoe', None) + self._is_on = bool(data.parameters.power.value) + if data.parameters.inside_temperature is not None: + self._inside_temperature = data.parameters.inside_temperature + if data.parameters.outside_temperature is not None: + self._outside_temperature = data.parameters.outside_temperature + if data.parameters.target_temperature is not None: + self._target_temperature = data.parameters.target_temperature + self._fan_mode = data.parameters.fan_speed.name + self._swing_mode = data.parameters.vertical_swing_mode.name + self._swing_lr_mode = data.parameters.horizontal_swing_mode.name + self._hvac_mode = data.parameters.mode.name + self._eco_mode = data.parameters.eco_mode.name + self._nanoe_mode = data.parameters.nanoe_mode.name except Exception as e: _LOGGER.debug("Failed to set data for device {id}".format(**self.device)) @@ -122,7 +122,7 @@ async def do_update_energy(self): _LOGGER.debug("Error trying to get device {id} state, probably expired token, trying to update it...".format(**self.device)) # noqa: E501 try: await self._api.refresh_token() - data= await self._api.get_device(self.id) # noqa: E501 + data= await self._api.history(self.id,"Month",today) # noqa: E501 except: _LOGGER.debug("Failed to renew token for device {id}, giving up for now".format(**self.device)) # noqa: E501 return @@ -246,6 +246,20 @@ def current_power(self): if not self.enable_energy_sensor: return None return self.current_power_value + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self.in_summer_house_mode: + return 8 + return 16 + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.in_summer_house_mode: + return 15 if self._details.features.summer_house == 2 else 10 + return 30 async def turn_off(self): await self.set_device( @@ -258,17 +272,92 @@ async def turn_on(self): { "power": self.constants.Power.On } ) await self.do_update() - + + @cached_property + def available_presets(self): + presets = [PRESET_NONE] + if self._details.features.quiet_mode: + presets.append(PRESET_ECO) + if self._details.features.powerful_mode: + presets.append(PRESET_BOOST) + if self._details.features.summer_house > 0: + presets.append(PRESET_8_15) + return presets + + @property + def preset_mode(self): + if not self._details: + return PRESET_NONE + match self._details.parameters.eco_mode: + case constants.EcoMode.Quiet: + return PRESET_ECO + case constants.EcoMode.Powerful: + return PRESET_BOOST + if self.in_summer_house_mode: + return PRESET_8_15 + + return PRESET_NONE + + @property + def in_summer_house_mode(self): + if not self._details: + return False + temp = self._details.parameters.target_temperature + i = 1 if temp - 8 > 0 else (0 if temp -8 else -1) + match self._details.features.summer_house: + case 1: + return i == 0 or temp == 10 + case 2: + return i >= 0 and temp <= 15 + case 3: + return i == 0 or temp == 10 + return False + async def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" _LOGGER.debug("Set %s ecomode %s", self.name, preset_mode) - await self.set_device( - { - "power": self.constants.Power.On, - "eco": self.constants.EcoMode[ PRESET_LIST[preset_mode] ] - }) + data = { + "power": constants.Power.On + } + if self.in_summer_house_mode and preset_mode != PRESET_8_15: + await self._exit_summer_house_mode(data) + + if PRESET_LIST[preset_mode] in self.constants.EcoMode: + data["eco"] = constants.EcoMode[ PRESET_LIST[preset_mode] ] + elif preset_mode == PRESET_8_15: + await self._enter_summer_house_mode() + data["mode"] = constants.OperationMode.Heat + data["eco"] = constants.EcoMode.Auto + data["temperature"] = 8 + data["fanSpeed"] = constants.FanSpeed.High + + await self.set_device(data) await self.do_update() + async def _enter_summer_house_mode(self): + data = await self._store.async_load() + if data is None: + data = {} + data['mode'] = self._details.parameters.mode + data['ecoMode'] = self._details.parameters.eco_mode + data['targetTemperature'] = self._details.parameters.target_temperature + data['fanSpeed'] = self._details.parameters.fan_speed + await self._store.async_save(data) + + async def _exit_summer_house_mode(self, device_data): + stored_data = await self._store.async_load() + if stored_data is None: + return + if 'mode' in stored_data: + device_data['mode'] = stored_data['mode'] + if 'ecoMode' in stored_data: + device_data["eco"] = stored_data['ecoMode'] + if 'targetTemperature' in stored_data: + device_data['temperature'] = stored_data['targetTemperature'] + if 'fanSpeed' in stored_data: + device_data['fanSpeed'] = stored_data['fanSpeed'] + + async def set_temperature(self, **kwargs): """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) @@ -302,12 +391,14 @@ async def set_fan_mode(self, fan_mode): async def set_hvac_mode(self, hvac_mode): """Set operation mode.""" _LOGGER.debug("Set %s mode %s", self.name, hvac_mode) + data = { + 'power': self.constants.Power.On + } + if self.in_summer_house_mode: + await self._exit_summer_house_mode(data) + data['mode'] = self.constants.OperationMode[OPERATION_LIST[hvac_mode]] - await self.set_device( - { - "power": self.constants.Power.On, - "mode": self.constants.OperationMode[OPERATION_LIST[hvac_mode]] - }) + await self.set_device(data) await self.do_update() diff --git a/custom_components/panasonic_cc/pcomfortcloud/apiclient.py b/custom_components/panasonic_cc/pcomfortcloud/apiclient.py index 6d8445d..911e269 100644 --- a/custom_components/panasonic_cc/pcomfortcloud/apiclient.py +++ b/custom_components/panasonic_cc/pcomfortcloud/apiclient.py @@ -10,6 +10,7 @@ from . import constants from . import panasonicsession +from .panasonicdevice import PanasonicDevice _current_time_zone = None def get_current_time_zone(): @@ -116,15 +117,12 @@ async def history(self, device_id, mode, date, time_zone=""): - async def get_device(self, device_id): + async def get_device(self, device_id) -> PanasonicDevice: device_guid = self._device_indexer.get(device_id) if device_guid: json_response = await self.execute_get(self._get_device_status_url(device_guid), "get_device", 200) - return { - 'id': device_id, - 'parameters': self._read_parameters(json_response['parameters']) - } + return PanasonicDevice(device_id, json_response) return None async def set_device(self, device_id, **kwargs): @@ -173,10 +171,10 @@ async def set_device(self, device_id, **kwargs): fan_auto = 0 device = await self.get_device(device_id) - if device and device['parameters']['airSwingHorizontal'].value == -1: + if device and device.parameters.horizontal_swing_mode == constants.AirSwingLR.Auto: fan_auto = fan_auto | 1 - if device and device['parameters']['airSwingVertical'].value == -1: + if device and device.parameters.vertical_swing_mode == constants.AirSwingUD.Auto: fan_auto = fan_auto | 2 if air_x is not None: diff --git a/custom_components/panasonic_cc/pcomfortcloud/constants.py b/custom_components/panasonic_cc/pcomfortcloud/constants.py index 5368a6e..4ca8a68 100644 --- a/custom_components/panasonic_cc/pcomfortcloud/constants.py +++ b/custom_components/panasonic_cc/pcomfortcloud/constants.py @@ -69,6 +69,8 @@ class NanoeMode(Enum): ModeG = 3 All = 4 +INVALID_TEMPERATURE = 126 + DEFAULT_X_APP_VERSION = "1.21.0" MAX_VERSION_AGE = 5 diff --git a/custom_components/panasonic_cc/pcomfortcloud/panasonicdevice.py b/custom_components/panasonic_cc/pcomfortcloud/panasonicdevice.py new file mode 100644 index 0000000..9088810 --- /dev/null +++ b/custom_components/panasonic_cc/pcomfortcloud/panasonicdevice.py @@ -0,0 +1,140 @@ +import logging + +from . import constants + +_LOGGER = logging.getLogger(__name__) + +class PanasonicDevice: + def __init__(self, id: str, json = None) -> None: + self.id = id + self.features = PanasonicDeviceFeatures(json) + json_parameters = None + if (json is not None and 'parameters' in json): + json_parameters = json['parameters'] + self.parameters = PanasonicDeviceParameters(json_parameters) + +class PanasonicDeviceFeatures: + def __init__(self, json = None) -> None: + self.permission = 0 + self.summer_house = 0 + self.iAutoX = False + self.nanoe = False + self.nanoe_stand_alone = False + self.auto_mode = False + self.heat_mode = False + self.fan_mode = False + self.dry_mode = False + self.cool_mode = False + self.eco_navi = False + self.powerful_mode = False + self.quiet_mode = False + self.air_swing_lr = False + self.auto_swing_ud = False + self.eco_eunction = 0 + self.load(json) + + def load(self, json): + if not json: + return + if 'permission' in json: + self.permission = json['permission'] + if 'summerHouse' in json: + self.summer_house = json['summerHouse'] + if 'iAutoX' in json: + self.iAutoX = json['iAutoX'] + if 'nanoe' in json: + self.nanoe = json['nanoe'] + if 'nanoeStandAlone' in json: + self.nanoe_stand_alone = json['nanoeStandAlone'] + if 'autoMode' in json: + self.auto_mode = json['autoMode'] + if 'heatMode' in json: + self.heat_mode = json['heatMode'] + if 'fanMode' in json: + self.fan_mode = json['fanMode'] + if 'dryMode' in json: + self.dry_mode = json['dryMode'] + if 'coolMode' in json: + self.cool_mode = json['coolMode'] + if 'ecoNavi' in json: + self.eco_navi = json['ecoNavi'] + if 'powerfulMode' in json: + self.powerful_mode = json['powerfulMode'] + if 'quietMode' in json: + self.quiet_mode = json['quietMode'] + if 'airSwingLR' in json: + self.air_swing_lr = json['airSwingLR'] + if 'autoSwingUD' in json: + self.auto_swing_ud = json['autoSwingUD'] + if 'ecoFunction' in json: + self.eco_eunction = json['ecoFunction'] + +class PanasonicDeviceParameters: + def __init__(self, json = None) -> None: + self.power = constants.Power.Off + self.mode = constants.OperationMode.Auto + self.fan_speed = constants.FanSpeed.Auto + self.horizontal_swing_mode = constants.AirSwingLR.Mid + self.vertical_swing_mode = constants.AirSwingUD.Mid + self.eco_mode = constants.EcoMode.Auto + self.nanoe_mode = constants.NanoeMode.Unavailable + self.target_temperature: int = None + self.inside_temperature: int = None + self.outside_temperature: int = None + self.load(json) + + + def load(self, json): + if not json: + return + if 'operate' in json: + self.power = constants.Power(json['operate']) + if 'operationMode' in json: + self.mode = constants.OperationMode(json['operationMode']) + if 'fanSpeed' in json: + self.fan_speed = constants.FanSpeed(json['fanSpeed']) + + self._load_swing_mode(json) + self._load_temperature(json) + + if 'ecoMode' in json: + self.eco_mode = constants.EcoMode(json['ecoMode']) + if 'nanoe' in json: + self.nanoe_mode = constants.NanoeMode(json['nanoe']) + + def _load_temperature(self, json): + if 'temperatureSet' in json and json['temperatureSet'] != constants.INVALID_TEMPERATURE: + self.target_temperature = json['temperatureSet'] + if 'insideTemperature' in json and json['insideTemperature'] != constants.INVALID_TEMPERATURE: + self.inside_temperature = json['insideTemperature'] + if 'outTemperature' in json and json['outTemperature'] != constants.INVALID_TEMPERATURE: + self.outside_temperature = json['outTemperature'] + + if self.inside_temperature is None and self.outside_temperature is not None: + self.inside_temperature = self.outside_temperature + self.outside_temperature = None + + + + def _load_swing_mode(self, json): + if 'airSwingLR' in json: + try: + self.horizontal_swing_mode = constants.AirSwingLR(json['airSwingLR']) + except: + _LOGGER.warning("Invalid horizontal swing mode '%s'", json['airSwingLR']) + if 'airSwingUD' in json: + try: + self.vertical_swing_mode = constants.AirSwingUD(json['airSwingUD']) + except: + _LOGGER.warning("Invalid vertical swing mode '%s'", json['airSwingUD']) + if 'fanAutoMode' in json: + if json['fanAutoMode'] == constants.AirSwingAutoMode.Both.value: + self.horizontal_swing_mode = constants.AirSwingLR.Auto + self.vertical_swing_mode = constants.AirSwingUD.Auto + elif json['fanAutoMode'] == constants.AirSwingAutoMode.AirSwingLR.value: + self.horizontal_swing_mode = constants.AirSwingLR.Auto + elif json['fanAutoMode'] == constants.AirSwingAutoMode.AirSwingUD.value: + self.vertical_swing_mode = constants.AirSwingUD.Auto + + +