diff --git a/custom_components/echonetlite/__init__.py b/custom_components/echonetlite/__init__.py index d7824c6..7d99c19 100644 --- a/custom_components/echonetlite/__init__.py +++ b/custom_components/echonetlite/__init__.py @@ -9,9 +9,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import Throttle -from .const import DOMAIN +from .const import DOMAIN, USER_OPTIONS, TEMP_OPTIONS from aioudp import UDPServer -# from pychonet import Factory + from pychonet import ECHONETAPIClient from pychonet.EchonetInstance import ( ENL_GETMAP, @@ -55,8 +55,6 @@ ENL_STATUS, ENL_BRIGHTNESS, ENL_COLOR_TEMP ] - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) host = None @@ -64,8 +62,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: loop = None server = None + _LOGGER.debug(f"pychonet version {VERSION}") if DOMAIN in hass.data: # maybe set up by config entry? - _LOGGER.debug(f"{hass.data[DOMAIN]} has already been setup..") + _LOGGER.debug(f"{hass.data[DOMAIN]} is already running.") server = hass.data[DOMAIN]['api'] hass.data[DOMAIN].update({entry.entry_id: []}) else: # setup API @@ -77,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server = ECHONETAPIClient(server=udp, loop=loop) server._message_timeout = 300 hass.data[DOMAIN].update({"api": server}) - _LOGGER.debug(f"pychonet version in use is {VERSION}") + for instance in entry.data["instances"]: echonetlite = None @@ -88,7 +87,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: getmap = instance["getmap"] setmap = instance["setmap"] uid = instance["uid"] - _LOGGER.debug(f'{instance["uid"]} is the UID..') # manually update API states using config entry data if host not in list(server._state): @@ -155,40 +153,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -# TODO FIX CODE REPETITION and update for Air Cleaner +# TODO update for Air Cleaner async def update_listener(hass, entry): for instance in hass.data[DOMAIN][entry.entry_id]: if instance['instance']['eojgc'] == 1 and instance['instance']['eojcc'] == 48: - if entry.options.get("fan_settings") is not None: # check if options has been created - if len(entry.options.get("fan_settings")) > 0: # if it has been created then check list length. - instance["echonetlite"]._user_options.update({ENL_FANSPEED: entry.options.get("fan_settings")}) - else: - instance["echonetlite"]._user_options.update({ENL_FANSPEED: False}) - - if entry.options.get("swing_horiz") is not None: - if len(entry.options.get("swing_horiz")) > 0: - instance["echonetlite"]._user_options.update({ENL_AIR_HORZ: entry.options.get("swing_horiz")}) - else: - instance["echonetlite"]._user_options.update({ENL_AIR_HORZ: False}) - - if entry.options.get("swing_vert") is not None: - if len(entry.options.get("swing_vert")) > 0: - instance["echonetlite"]._user_options.update({ENL_AIR_VERT: entry.options.get("swing_vert")}) - else: - instance["echonetlite"]._user_options.update({ENL_AIR_VERT: False}) - - if entry.options.get("auto_direction") is not None: - if len(entry.options.get("auto_direction")) > 0: - instance["echonetlite"]._user_options.update({ENL_AUTO_DIRECTION: entry.options.get("auto_direction")}) - else: - instance["echonetlite"]._user_options.update({ENL_AUTO_DIRECTION: False}) - - if entry.options.get("swing_mode") is not None: - if len(entry.options.get("swing_mode")) > 0: - instance["echonetlite"]._user_options.update({ENL_SWING_MODE: entry.options.get("swing_mode")}) - else: - instance["echonetlite"]._user_options.update({ENL_SWING_MODE: False}) - + for option in USER_OPTIONS.keys(): + if entry.options.get(USER_OPTIONS[option]["option"]) is not None: # check if options has been created + if len(entry.options.get(USER_OPTIONS[option]["option"])) > 0: # if it has been created then check list length. + instance["echonetlite"]._user_options.update({option: entry.options.get(USER_OPTIONS[option]["option"])}) + else: + instance["echonetlite"]._user_options.update({option: False}) + for option in TEMP_OPTIONS.keys(): + if entry.options.get(option) is not None: + instance["echonetlite"]._user_options.update({option: entry.options.get(option)}) class ECHONETConnector(): """EchonetAPIConnector is used to centralise API calls for Echonet devices. @@ -212,13 +189,13 @@ def __init__(self, instance, api, entry): self._update_flags_full_list = [] flags = [] if self._eojgc == 1 and self._eojcc == 48: - _LOGGER.debug(f"Create new HomeAirConditioner instance at: {self._host}") + _LOGGER.debug(f"Starting ECHONETLite HomeAirConditioner instance at {self._host}") flags = HVAC_API_CONNECTOR_DEFAULT_FLAGS elif self._eojgc == 2 and self._eojcc == 144: - _LOGGER.debug(f"Create new GeneralLighting instance at: {self._host}") + _LOGGER.debug(f"Starting ECHONETLite GeneralLighting instance at {self._host}") flags = LIGHT_API_CONNECTOR_DEFAULT_FLAGS else: - _LOGGER.debug(f"Create new Generic instance for {self._eojgc}-{self._eojcc}-{self._eojci} at {self._host}") + _LOGGER.debug(f"Starting ECHONETLite Generic instance for {self._eojgc}-{self._eojcc}-{self._eojci} at {self._host}") flags = [ENL_STATUS] for item in self._getPropertyMap: if item not in list(EPC_SUPER.keys()): @@ -240,33 +217,33 @@ def __init__(self, instance, api, entry): start_index += MAX_UPDATE_BATCH_SIZE self._update_flag_batches.append(self._update_flags_full_list[start_index:full_list_length]) + # TODO this looks messy. self._user_options = { ENL_FANSPEED: False, ENL_AUTO_DIRECTION: False, ENL_SWING_MODE: False, ENL_AIR_VERT: False, - ENL_AIR_HORZ: False + ENL_AIR_HORZ: False, + 'min_temp_heat': 15, + 'max_temp_heat': 35, + 'min_temp_cool': 15, + 'max_temp_cool': 35, + 'min_temp_auto': 15, + 'max_temp_auto': 35, } - # Stitch together user selectable options for fan + swing modes for HVAC - # TODO - fix code repetition - if entry.options.get("fan_settings") is not None: # check if options has been created - if len(entry.options.get("fan_settings")) > 0: # if it has been created then check list length. - self._user_options[ENL_FANSPEED] = entry.options.get("fan_settings") - if entry.options.get("swing_horiz") is not None: - if len(entry.options.get("swing_horiz")) > 0: - self._user_options[ENL_AIR_HORZ] = entry.options.get("swing_horiz") - if entry.options.get("swing_vert") is not None: # check if options has been created - if len(entry.options.get("swing_vert")) > 0: - self._user_options[ENL_AIR_VERT] = entry.options.get("swing_vert") - if entry.options.get("auto_direction") is not None: # check if options has been created - if len(entry.options.get("auto_direction")) > 0: - self._user_options[ENL_AUTO_DIRECTION] = entry.options.get("auto_direction") - if entry.options.get("swing_mode") is not None: # check if options has been created - if len(entry.options.get("swing_mode")) > 0: - self._user_options[ENL_SWING_MODE] = entry.options.get("swing_mode") + # User selectable options for fan + swing modes for HVAC + for option in USER_OPTIONS.keys(): + if entry.options.get(USER_OPTIONS[option]['option']) is not None: # check if options has been created + if len(entry.options.get(USER_OPTIONS[option]['option'])) > 0: # if it has been created then check list length. + self._user_options[option] = entry.options.get(USER_OPTIONS[option]['option']) + + # Temperature range options for heat, cool and auto modes + for option in TEMP_OPTIONS.keys(): + if entry.options.get(option) is not None: + self._user_options[option] = entry.options.get(option) self._uid = self._api._state[self._host]["instances"][self._eojgc][self._eojcc][self._eojci][ENL_UID] - _LOGGER.debug(f'{self._uid} is the UID in ECHONET connector..') + _LOGGER.debug(f'ECHONET instance UID is {self._uid}') if self._uid is None: self._uid = f"{self._host}-{self._eojgc}-{self._eojcc}-{self._eojci}" @@ -281,14 +258,14 @@ async def async_update(self, **kwargs): update_data.update(batch_data) elif len(flags) == 1: update_data[flags[0]] = batch_data - _LOGGER.debug(f"{list(update_data.values())}") + _LOGGER.debug(f"ECHONETlite polling update data - {list(update_data.values())}") if len(update_data) > 0 and False not in list(update_data.values()): # polling succeded. if retry > 1: - _LOGGER.debug(f"polling ECHONET Instance host {self._host} succeeded - Retry {retry} of 3") + _LOGGER.debug(f"Polling ECHONET Instance host {self._host} succeeded. Retry {retry} of 3") self._update_data.update(update_data) return self._update_data else: - _LOGGER.debug(f"polling ECHONET Instance host {self._host} timed out - Retry {retry} of 3") - _LOGGER.debug(f"Number of missed ECHONETLite msssages since reboot is - {len(self._api._message_list)}") + _LOGGER.debug(f"Polling ECHONET Instance host {self._host} timed out. Retry {retry} of 3") + _LOGGER.debug(f"Number of missed ECHONETLite msssages since reboot is {len(self._api._message_list)}") return self._update_data diff --git a/custom_components/echonetlite/climate.py b/custom_components/echonetlite/climate.py index af2dc12..a3f1d01 100644 --- a/custom_components/echonetlite/climate.py +++ b/custom_components/echonetlite/climate.py @@ -72,6 +72,9 @@ def __init__(self, name, connector, units: UnitSystem, fan_modes=None, swing_ver if ENL_AIR_VERT in list(self._connector._setPropertyMap): self._support_flags = self._support_flags | SUPPORT_SWING_MODE self._hvac_modes = DEFAULT_HVAC_MODES + self._min_temp = self._connector._user_options['min_temp_auto'] + self._max_temp = self._connector._user_options['max_temp_auto'] + async def async_update(self): """Get the latest state from the HVAC.""" @@ -257,3 +260,25 @@ async def async_turn_on(self): async def async_turn_off(self): """Turn off.""" await self._connector._instance.off() + + @property + def min_temp(self) -> int: + """Return the minimum temperature supported by the HVAC.""" + if self.hvac_mode == HVAC_MODE_HEAT: + self._min_temp = self._connector._user_options['min_temp_heat'] + if self.hvac_mode == HVAC_MODE_COOL: + self._min_temp = self._connector._user_options['min_temp_cool'] + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + self._min_temp = self._connector._user_options['min_temp_auto'] + return self._min_temp + + @property + def max_temp(self) -> int: + """Return the maximum temperature supported by the HVAC.""" + if self.hvac_mode == HVAC_MODE_HEAT: + self._max_temp = self._connector._user_options['max_temp_heat'] + if self.hvac_mode == HVAC_MODE_COOL: + self._max_temp = self._connector._user_options['max_temp_cool'] + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + self._max_temp = self._connector._user_options['max_temp_auto'] + return self._max_temp diff --git a/custom_components/echonetlite/config_flow.py b/custom_components/echonetlite/config_flow.py index d921346..21bdf64 100644 --- a/custom_components/echonetlite/config_flow.py +++ b/custom_components/echonetlite/config_flow.py @@ -16,7 +16,7 @@ from aioudp import UDPServer # from pychonet import Factory from pychonet import ECHONETAPIClient -from .const import DOMAIN, USER_OPTIONS +from .const import DOMAIN, USER_OPTIONS, TEMP_OPTIONS _LOGGER = logging.getLogger(__name__) @@ -163,6 +163,19 @@ async def async_step_init(self, user_input=None): USER_OPTIONS[option]['option_list'] ) }) + + # Handle setting temperature ranges for various modes of operation + for option in list(TEMP_OPTIONS.keys()): + default_temp = TEMP_OPTIONS[option]['min'] + if self._config_entry.options.get(option) is not None: + default_temp = self._config_entry.options.get(option) + data_schema_structure.update({ + vol.Required( + option, + default=default_temp + ): vol.All(vol.Coerce(int), vol.Range(min=TEMP_OPTIONS[option]['min'], max=TEMP_OPTIONS[option]['max'])) + }) + elif instance['eojgc'] == 0x01 and instance['eojcc'] == 0x35: # AirCleaner for option in list(USER_OPTIONS.keys()): if option in instance['setmap']: diff --git a/custom_components/echonetlite/const.py b/custom_components/echonetlite/const.py index 03f36c7..4142140 100644 --- a/custom_components/echonetlite/const.py +++ b/custom_components/echonetlite/const.py @@ -81,7 +81,7 @@ } }, 'default': { - CONF_ICON: None, + CONF_ICON: None, CONF_TYPE: None, CONF_STATE_CLASS: None, }, @@ -157,3 +157,11 @@ ENL_AUTO_DIRECTION: {'option': 'auto_direction', 'option_list': AUTO_DIRECTION_OPTIONS}, ENL_SWING_MODE: {'option': 'swing_mode', 'option_list': SWING_MODE_OPTIONS}, } + +TEMP_OPTIONS = {"min_temp_heat": {"min":15, "max":20}, + "max_temp_heat": {"min":25, "max":35}, + "min_temp_cool": {"min":15, "max":20}, + "max_temp_cool": {"min":25, "max":35}, + "min_temp_auto": {"min":15, "max":20}, + "max_temp_auto": {"min":25, "max":35}, +} diff --git a/custom_components/echonetlite/light.py b/custom_components/echonetlite/light.py index c8cdd3c..2fd1047 100644 --- a/custom_components/echonetlite/light.py +++ b/custom_components/echonetlite/light.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): entities = [] for entity in hass.data[DOMAIN][config_entry.entry_id]: if entity['instance']['eojgc'] == 0x02 and entity['instance']['eojcc'] == 0x90: # General Lighting - _LOGGER.debug("Found ECHONET Light") + _LOGGER.debug("Configuring ECHONETlite Light entity") entities.append(EchonetLight(config_entry.title, entity['echonetlite'])) _LOGGER.debug(f"Number of light devices to be added: {len(entities)}") async_add_devices(entities, True) @@ -129,7 +129,7 @@ async def async_turn_on(self, **kwargs): if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in self._supported_color_modes: # bring the selected color to something we can calculate on color_scale = (float(kwargs[ATTR_COLOR_TEMP]) - float(self._min_mireds)) / float(self._max_mireds - self._min_mireds) - _LOGGER.debug(f"set color to : {color_scale}") + _LOGGER.debug(f"Set color to : {color_scale}") # bring the color to color_scale_echonet = color_scale * (len(self._echonet_mireds) - 1) # round it to an index diff --git a/custom_components/echonetlite/manifest.json b/custom_components/echonetlite/manifest.json index 08945df..14d742e 100644 --- a/custom_components/echonetlite/manifest.json +++ b/custom_components/echonetlite/manifest.json @@ -12,6 +12,6 @@ "codeowners": [ "@scottyphillips" ], - "version": "3.3.1", + "version": "3.4.0", "iot_class": "local_polling" } diff --git a/custom_components/echonetlite/sensor.py b/custom_components/echonetlite/sensor.py index e06540a..216aba1 100644 --- a/custom_components/echonetlite/sensor.py +++ b/custom_components/echonetlite/sensor.py @@ -24,14 +24,14 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): entities = [] for entity in hass.data[DOMAIN][config.entry_id]: - _LOGGER.debug(f"setting up sensor {entity}") - _LOGGER.debug(f"update flags for this sensor are {entity['echonetlite']._update_flags_full_list}") + _LOGGER.debug(f"Configuring ECHONETLite sensor {entity}") + _LOGGER.debug(f"Update flags for this sensor are {entity['echonetlite']._update_flags_full_list}") eojgc = entity['instance']['eojgc'] eojcc = entity['instance']['eojcc'] # Home Air Conditioner we dont bother exposing all sensors if eojgc == 1 and eojcc == 48: - _LOGGER.debug("This is an ECHONET climate device so only a few sensors will be created") + _LOGGER.debug("This is an ECHONET climate device so not all sensors will be configured.") for op_code in ENL_SENSOR_OP_CODES[eojgc][eojcc].keys(): if op_code in entity['instance']['getmap']: entities.append( @@ -43,7 +43,7 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non ) ) elif eojgc == 1 and eojcc == 53: - _LOGGER.debug("This is an ECHONET fan device so only a few sensors will be created") + _LOGGER.debug("This is an ECHONET fan device so not all sensors will be configured.") for op_code in ENL_SENSOR_OP_CODES[eojgc][eojcc].keys(): if op_code in entity['instance']['getmap']: entities.append( @@ -55,7 +55,6 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non ) ) else: # handle other ECHONET instances - _LOGGER.debug("Configuring ECHONETlite sensor..") for op_code in EPC_CODE[eojgc][eojcc]: if eojgc in ENL_SENSOR_OP_CODES.keys(): if eojcc in ENL_SENSOR_OP_CODES[eojgc].keys(): @@ -171,7 +170,7 @@ def native_value(self) -> StateType: return self._instance._update_data[self._op_code] else: return STATE_UNAVAILABLE - return None + return STATE_UNAVAILABLE @property def native_unit_of_measurement(self): diff --git a/custom_components/echonetlite/strings.json b/custom_components/echonetlite/strings.json index 0cecd13..f139e89 100644 --- a/custom_components/echonetlite/strings.json +++ b/custom_components/echonetlite/strings.json @@ -33,7 +33,13 @@ "swing_horiz": "Configure Horizontal Swing Settings", "swing_vert": "Configure Vertical Swing Settings", "auto_direction": "Configure Auto Direction", - "swing_mode": "Configure Swing Mode" + "swing_mode": "Configure Swing Mode", + "min_temp_heat": "Configure Minimum Temperature for Heating Operation", + "max_temp_heat": "Configure Maximum Temperature for Heating Operation", + "min_temp_cool": "Configure Minimum Temperature for Cooling Operation", + "max_temp_cool": "Configure Maximum Temperature for Cooling Operation", + "min_temp_auto": "Configure Minimum Temperature for Automatic Operation", + "max_temp_auto": "Configure Maximum Temperature for Automatic Operation" }, "description": "Configure Fan Settings" } diff --git a/custom_components/echonetlite/translations/en.json b/custom_components/echonetlite/translations/en.json index de1dec7..f085686 100644 --- a/custom_components/echonetlite/translations/en.json +++ b/custom_components/echonetlite/translations/en.json @@ -32,11 +32,17 @@ "swing_horiz": "Configure Horizontal Swing Settings", "swing_vert": "Configure Vertical Swing Settings", "auto_direction": "Configure Auto Direction", - "swing_mode": "Configure Swing Mode" + "swing_mode": "Configure Swing Mode", + "min_temp_heat": "Configure Minimum Temperature for Heating Operation", + "max_temp_heat": "Configure Maximum Temperature for Heating Operation", + "min_temp_cool": "Configure Minimum Temperature for Cooling Operation", + "max_temp_cool": "Configure Maximum Temperature for Cooling Operation", + "min_temp_auto": "Configure Minimum Temperature for Automatic Operation", + "max_temp_auto": "Configure Maximum Temperature for Automatic Operation" }, "description": "Configure optional fan and swing mode settings" } } }, "title": "ECHONETLite" -} \ No newline at end of file +}