diff --git a/custom_components/echonetlite/const.py b/custom_components/echonetlite/const.py index 41de37c..6ed92c0 100644 --- a/custom_components/echonetlite/const.py +++ b/custom_components/echonetlite/const.py @@ -48,8 +48,14 @@ from pychonet.lib.const import ENL_CUMULATIVE_POWER, ENL_INSTANTANEOUS_POWER from pychonet.lib.epc_functions import DATA_STATE_CLOSE, DATA_STATE_OPEN from pychonet.CeilingFan import ( + ENL_BUZZER, ENL_FANSPEED_PERCENT, ENL_FAN_DIRECTION, + ENL_FAN_LIGHT_BRIGHTNESS, + ENL_FAN_LIGHT_COLOR_TEMP, + ENL_FAN_LIGHT_MODE, + ENL_FAN_LIGHT_NIGHT_BRIGHTNESS, + ENL_FAN_LIGHT_STATUS, ENL_FAN_OSCILLATION, ) @@ -1190,7 +1196,17 @@ # Home Air Conditioner 0x30: {ENL_HVAC_MODE, ENL_HVAC_SET_TEMP, ENL_HVAC_SILENT_MODE}, # Ceiling fan - 0x3A: {ENL_FANSPEED_PERCENT, ENL_FAN_DIRECTION, ENL_FAN_OSCILLATION}, + 0x3A: { + ENL_FANSPEED_PERCENT, + ENL_FAN_DIRECTION, + ENL_FAN_OSCILLATION, + ENL_FAN_LIGHT_STATUS, + ENL_FAN_LIGHT_MODE, + ENL_FAN_LIGHT_BRIGHTNESS, + ENL_FAN_LIGHT_COLOR_TEMP, + ENL_FAN_LIGHT_NIGHT_BRIGHTNESS, + ENL_BUZZER, + }, }, 0x02: { 0x60: {ENL_OPENING_LEVEL, ENL_BLIND_ANGLE, ENL_OPENCLOSE_STATUS}, diff --git a/custom_components/echonetlite/fan.py b/custom_components/echonetlite/fan.py index 0c99ddb..cdb0a04 100644 --- a/custom_components/echonetlite/fan.py +++ b/custom_components/echonetlite/fan.py @@ -3,6 +3,11 @@ from pychonet.lib.const import ENL_STATUS from pychonet.lib.eojx import EOJX_CLASS +from pychonet.CeilingFan import ( + ENL_FANSPEED_PERCENT, + ENL_FAN_DIRECTION, + ENL_FAN_OSCILLATION, +) from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.const import ( PRECISION_WHOLE, @@ -13,9 +18,6 @@ DATA_STATE_ON, DOMAIN, ENL_FANSPEED, - ENL_FANSPEED_PERCENT, - ENL_FAN_DIRECTION, - ENL_FAN_OSCILLATION, ) _LOGGER = logging.getLogger(__name__) @@ -71,6 +73,8 @@ def __init__(self, connector, config): self._attr_should_poll = True self._attr_available = True + self._attr_speed_count = getattr(self._connector._instance, "SPEED_COUNT", 100) + self._set_attrs() self.update_option_listener() diff --git a/custom_components/echonetlite/light.py b/custom_components/echonetlite/light.py index 6e3d9af..c1c551a 100644 --- a/custom_components/echonetlite/light.py +++ b/custom_components/echonetlite/light.py @@ -1,10 +1,23 @@ import logging from pychonet.GeneralLighting import ENL_STATUS, ENL_BRIGHTNESS, ENL_COLOR_TEMP +from pychonet.CeilingFan import ( + ENL_FAN_LIGHT_STATUS, + ENL_FAN_LIGHT_BRIGHTNESS, + ENL_FAN_LIGHT_COLOR_TEMP, +) + +from pychonet.lib.const import ENL_ON from pychonet.lib.eojx import EOJX_CLASS +from pychonet.lib.epc_functions import _swap_dict -from homeassistant.components.light import LightEntity, ColorMode, LightEntityFeature +from homeassistant.components.light import ( + ATTR_EFFECT, + LightEntity, + ColorMode, + LightEntityFeature, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,8 +31,8 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_BRIGHTNESS_SCALE = 255 -MIN_MIREDS = 153 -MAX_MIREDS = 500 +MIN_MIREDS = 153 # 6500k +MAX_MIREDS = 500 # 2000k DEVICE_SCALE = 100 @@ -29,13 +42,55 @@ async def async_setup_entry(hass, config_entry, async_add_devices): for entity in hass.data[DOMAIN][config_entry.entry_id]: eojgc = entity["instance"]["eojgc"] eojcc = entity["instance"]["eojcc"] - if eojgc == 0x02 and eojcc in (0x90, 0x91): - # General Lighting (0x90), Mono Functional Lighting (0x91) + if (eojgc == 0x02 and eojcc in (0x90, 0x91, 0xA3)) or ( + eojgc == 0x01 + and eojcc == 0x3A + and ENL_FAN_LIGHT_STATUS in entity["echonetlite"]._setPropertyMap + ): + custom_options = {} + # General Lighting (0x90), Mono Functional Lighting (0x91), Lighting System (0xA3) + if eojgc == 0x02 and eojcc in (0x90, 0x91, 0xA3): + custom_options = { + ENL_STATUS: ENL_STATUS, + ENL_BRIGHTNESS: ENL_BRIGHTNESS, + ENL_COLOR_TEMP: ENL_COLOR_TEMP, + "echonet_color": { + 0x44: "daylight_color", + 0x43: "daylight_white", + 0x42: "white", + 0x40: "other", + 0x41: "incandescent_lamp_color", + }, + "echonet_mireds_int": { + 0x44: 153, # 6500K + 0x43: 200, # 5000K + 0x42: 238, # 4200K + 0x40: 285, # 3500K + 0x41: 370, # 2700K + }, # coolest to warmest value is mired + "on": "on", + "off": "off", + } + custom_options["echonet_int_color"] = _swap_dict( + custom_options["echonet_color"] + ) + # Ceiling Fan (0x01-0x3A) + elif eojgc == 0x01 and eojcc == 0x3A: + custom_options = { + ENL_STATUS: ENL_FAN_LIGHT_STATUS, + ENL_BRIGHTNESS: ENL_FAN_LIGHT_BRIGHTNESS, + ENL_COLOR_TEMP: ENL_FAN_LIGHT_COLOR_TEMP, + "echonet_color": None, + "echonet_mireds_int": None, + "on": "light_on", + "off": "light_off", + } _LOGGER.debug("Configuring ECHONETlite Light entity") entities.append( EchonetLight( entity["echonetlite"], config_entry, + custom_options, ) ) _LOGGER.debug(f"Number of light devices to be added: {len(entities)}") @@ -45,7 +100,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): class EchonetLight(LightEntity): """Representation of a ECHONET light device.""" - def __init__(self, connector, config): + def __init__(self, connector, config, custom_options): """Initialize the climate device.""" name = get_device_name(connector, config) self._attr_name = name @@ -58,32 +113,49 @@ def __init__(self, connector, config): self._server_state = self._connector._api._state[ self._connector._instance._host ] - self._attr_min_mireds = MIN_MIREDS - self._attr_max_mireds = MAX_MIREDS - self._attr_supported_color_modes.add(ColorMode.ONOFF) - self._attr_color_mode = ColorMode.ONOFF - if ENL_BRIGHTNESS in list(self._connector._setPropertyMap): - self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) - self._attr_color_mode = ColorMode.BRIGHTNESS - if ENL_COLOR_TEMP in list(self._connector._setPropertyMap): + if mireds_int := custom_options.get("echonet_mireds_int"): + mireds = mireds_int.values() + self._attr_min_mireds = min(mireds) + self._attr_max_mireds = max(mireds) + else: + self._attr_min_mireds = MIN_MIREDS + self._attr_max_mireds = MAX_MIREDS + self._custom_options = custom_options + if custom_options[ENL_COLOR_TEMP] in list(self._connector._setPropertyMap): self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_color_mode = ColorMode.COLOR_TEMP + if custom_options[ENL_BRIGHTNESS] in list(self._connector._setPropertyMap): + if not self._attr_supported_color_modes: + self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) + self._attr_color_mode = ColorMode.BRIGHTNESS + if not self._attr_supported_color_modes: + self._attr_supported_color_modes.add(ColorMode.ONOFF) + self._attr_color_mode = ColorMode.ONOFF - self._echonet_mireds = [ - "daylight_color", - "daylight_white", - "white", - "other", - "incandescent_lamp_color", - ] # coolest to warmest - self._echonet_mireds_int = [68, 67, 66, 64, 65] # coolest to warmest self._olddata = {} self._attr_is_on = ( - True if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON else False + True + if self._connector._update_data[custom_options[ENL_STATUS]] == DATA_STATE_ON + else False ) + + if hasattr(self._connector._instance, "getEffectList"): + self._attr_effect_list = self._connector._instance.getEffectList() + if self._attr_effect_list: + self._attr_supported_features |= LightEntityFeature.EFFECT + + if hasattr(self._connector._instance, "getLightColorLevelMax"): + self._light_color_level_max = ( + self._connector._instance.getLightColorLevelMax() + ) + else: + self._light_color_level_max = 100 + self._attr_should_poll = True self._attr_available = True + self._set_attrs() + self.update_option_listener() async def async_update(self): @@ -119,13 +191,12 @@ def device_info(self): } async def async_turn_on(self, **kwargs): - """Turn on.""" - await self._connector._instance.on() + states = {"status": ENL_ON} if ( ATTR_BRIGHTNESS in kwargs and self._attr_supported_color_modes - and ColorMode.BRIGHTNESS in self._attr_supported_color_modes + and self._attr_color_mode in {ColorMode.BRIGHTNESS, ColorMode.COLOR_TEMP} ): normalized_brightness = ( float(kwargs[ATTR_BRIGHTNESS]) / DEFAULT_BRIGHTNESS_SCALE @@ -135,46 +206,82 @@ async def async_turn_on(self, **kwargs): device_brightness = max(device_brightness, 1) # send the message to the lamp - await self._connector._instance.setBrightness(device_brightness) + states["brightness"] = device_brightness self._attr_brightness = kwargs[ATTR_BRIGHTNESS] if ( ATTR_COLOR_TEMP in kwargs and self._attr_supported_color_modes - and ColorMode.COLOR_TEMP in self._attr_supported_color_modes + and self._attr_color_mode == ColorMode.COLOR_TEMP ): - # bring the selected color to something we can calculate on - color_scale = ( - float(kwargs[ATTR_COLOR_TEMP]) - float(self._attr_min_mireds) - ) / float(self._attr_max_mireds - self._attr_min_mireds) - _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 - echonet_idx = round(color_scale_echonet) - color_temp = self._echonet_mireds[echonet_idx] - color_temp_int = self._echonet_mireds_int[echonet_idx] - - _LOGGER.debug(f"New color temp of light: {color_temp} - {color_temp_int}") - await self._connector._instance.setColorTemperature(color_temp_int) - self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] + attr_color_tmp = float(kwargs[ATTR_COLOR_TEMP]) + if self._custom_options["echonet_color"]: + color_temp_int = 0x41 + for i, mired in self._custom_options["echonet_mireds_int"].items(): + if attr_color_tmp <= mired + 15: + color_temp_int = i + break + color_temp = self._custom_options["echonet_color"].get(color_temp_int) + _LOGGER.debug( + f"New color temp of light: {color_temp} - {color_temp_int}" + ) + self._attr_color_temp = int( + self._custom_options["echonet_mireds_int"].get(color_temp_int) + ) + else: + color_scale = (attr_color_tmp - float(self._attr_min_mireds)) / float( + self._attr_max_mireds - self._attr_min_mireds + ) + _LOGGER.debug(f"Set color to : {color_scale}") + color_temp_int = min( + self._light_color_level_max, + max(1, (1 - color_scale) * self._light_color_level_max), + ) + _LOGGER.debug( + f"New color temp of light: {attr_color_tmp} mireds - {color_temp_int}" + ) + self._attr_color_temp = int(attr_color_tmp) + + states["color_temperature"] = int(color_temp_int) + + if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in self._attr_effect_list: + states[ATTR_EFFECT] = kwargs[ATTR_EFFECT] + + if hasattr(self._connector._instance, "setLightStates"): + return await self._connector._instance.setLightStates(states) + else: + """Turn on.""" + result = await getattr( + self._connector._instance, self._custom_options["on"] + )() + + if result: + if states.get("brightness"): + result &= await self._connector._instance.setBrightness( + states["brightness"] + ) + + if states.get("color_temperature"): + result &= await self._connector._instance.setColorTemperature( + states["color_temperature"] + ) async def async_turn_off(self, **kwargs): """Turn off.""" - await self._connector._instance.off() + await getattr(self._connector._instance, self._custom_options["off"])() def _set_attrs(self): - if ( - self._attr_supported_color_modes - and ColorMode.BRIGHTNESS in self._attr_supported_color_modes - ): + if self._attr_supported_color_modes and self._attr_color_mode in { + ColorMode.BRIGHTNESS, + ColorMode.COLOR_TEMP, + }: """brightness of this light between 0..255.""" _LOGGER.debug( - f"Current brightness of light: {self._connector._update_data[ENL_BRIGHTNESS]}" + f"Current brightness of light: {self._connector._update_data[self._custom_options[ENL_BRIGHTNESS]]}" ) brightness = ( - int(self._connector._update_data[ENL_BRIGHTNESS], 16) - if ENL_BRIGHTNESS in self._connector._update_data + int(self._connector._update_data[self._custom_options[ENL_BRIGHTNESS]]) + if self._custom_options[ENL_BRIGHTNESS] in self._connector._update_data else -1 ) if brightness >= 0: @@ -187,31 +294,37 @@ def _set_attrs(self): if ( self._attr_supported_color_modes - and ColorMode.COLOR_TEMP in self._attr_supported_color_modes + and self._attr_color_mode == ColorMode.COLOR_TEMP ): """color temperature in mired.""" + enl_color_temp = self._custom_options[ENL_COLOR_TEMP] _LOGGER.debug( - f"Current color temp of light: {self._connector._update_data[ENL_COLOR_TEMP]}" - ) - - # calculate some helper - mired_steps = (self._attr_max_mireds - self._attr_min_mireds) / float( - len(self._echonet_mireds) + f"Current color temp of light: {self._connector._update_data[enl_color_temp]}" ) - # get the current echonet mireds - color_temp = ( - self._connector._update_data[ENL_COLOR_TEMP] - if ENL_COLOR_TEMP in self._connector._update_data - else "white" - ) - if color_temp in self._echonet_mireds: - self._attr_color_temp = ( - round(self._echonet_mireds.index(color_temp) * mired_steps) - + MIN_MIREDS + if self._custom_options["echonet_color"]: + # get the current echonet mireds + color_temp = ( + self._connector._update_data[enl_color_temp] + if enl_color_temp in self._connector._update_data + else "white" + ) + self._attr_color_temp = self._custom_options["echonet_mireds_int"].get( + self._custom_options["echonet_int_color"].get(color_temp), 153 ) else: - self._attr_color_temp = MIN_MIREDS + self._attr_color_temp = ( + self._attr_max_mireds - self._attr_min_mireds + ) * ( + ( + self._light_color_level_max + - self._connector._update_data[enl_color_temp] + ) + / self._light_color_level_max + ) + self._attr_min_mireds + + if hasattr(self._connector._instance, "getEffect"): + self._attr_effect = self._connector._instance.getEffect() async def async_added_to_hass(self): """Register callbacks.""" @@ -228,7 +341,8 @@ async def async_update_callback(self, isPush: bool = False): self._olddata = self._connector._update_data.copy() self._attr_is_on = ( True - if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON + if self._connector._update_data[self._custom_options[ENL_STATUS]] + == DATA_STATE_ON else False ) if self._attr_available != self._server_state["available"]: @@ -247,11 +361,12 @@ async def async_update_callback(self, isPush: bool = False): def update_option_listener(self): _should_poll = ( - ENL_STATUS not in self._connector._ntfPropertyMap + self._custom_options[ENL_STATUS] not in self._connector._ntfPropertyMap or ( self._attr_supported_color_modes and COLOR_MODE_BRIGHTNESS in self._attr_supported_color_modes - and ENL_BRIGHTNESS not in self._connector._ntfPropertyMap + and self._custom_options[ENL_BRIGHTNESS] + not in self._connector._ntfPropertyMap ) or ( self._attr_supported_color_modes