diff --git a/custom_components/echonetlite/__init__.py b/custom_components/echonetlite/__init__.py index 7c0ecf5..4b8c33c 100644 --- a/custom_components/echonetlite/__init__.py +++ b/custom_components/echonetlite/__init__.py @@ -1,4 +1,5 @@ """The echonetlite integration.""" + from __future__ import annotations import logging import pychonet as echonet @@ -11,13 +12,23 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import Throttle -from homeassistant.const import Platform +from homeassistant.const import ( + Platform, + PERCENTAGE, + UnitOfPower, + UnitOfTemperature, + UnitOfEnergy, + UnitOfVolume, + UnitOfElectricCurrent, + UnitOfElectricPotential, +) from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.number import NumberDeviceClass from .const import ( DOMAIN, USER_OPTIONS, TEMP_OPTIONS, - CONF_FORCE_POLLING, CONF_BATCH_SIZE_MAX, MISC_OPTIONS, ) @@ -46,8 +57,6 @@ ENL_HVAC_ROOM_TEMP, ENL_HVAC_SILENT_MODE, ENL_HVAC_OUT_TEMP, - ENL_HVAC_HUMIDIFIER_STATE, - ENL_HVAC_HUMIDIFIER_VALUE, ) from pychonet.DistributionPanelMeter import ( @@ -64,7 +73,6 @@ from pychonet.LowVoltageSmartElectricEnergyMeter import ( ENL_LVSEEM_COEF, - ENL_LVSEEM_DIGITS, ENL_LVSEEM_ENG_UNIT, ENL_LVSEEM_ENG_NOR, ENL_LVSEEM_ENG_REV, @@ -84,6 +92,8 @@ Platform.LIGHT, Platform.FAN, Platform.SWITCH, + Platform.TIME, + Platform.NUMBER, ] PARALLEL_UPDATES = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) @@ -187,6 +197,52 @@ def polling_update_debug_log(values, eojgc, eojcc): return debug_log +def get_unit_by_devise_class(device_class: str) -> str | None: + if ( + device_class == SensorDeviceClass.TEMPERATURE + or device_class == NumberDeviceClass.TEMPERATURE + ): + unit = UnitOfTemperature.CELSIUS + elif ( + device_class == SensorDeviceClass.ENERGY + or device_class == NumberDeviceClass.ENERGY + ): + unit = UnitOfEnergy.WATT_HOUR + elif ( + device_class == SensorDeviceClass.POWER + or device_class == NumberDeviceClass.POWER + ): + unit = UnitOfPower.WATT + elif ( + device_class == SensorDeviceClass.CURRENT + or device_class == NumberDeviceClass.CURRENT + ): + unit = UnitOfElectricCurrent.AMPERE + elif ( + device_class == SensorDeviceClass.VOLTAGE + or device_class == NumberDeviceClass.VOLTAGE + ): + unit = UnitOfElectricPotential.VOLT + elif ( + device_class == SensorDeviceClass.HUMIDITY + or device_class == SensorDeviceClass.BATTERY + or device_class == NumberDeviceClass.HUMIDITY + or device_class == NumberDeviceClass.BATTERY + ): + unit = PERCENTAGE + elif device_class == SensorDeviceClass.GAS or device_class == NumberDeviceClass.GAS: + unit = UnitOfVolume.CUBIC_METERS + elif ( + device_class == SensorDeviceClass.WATER + or device_class == NumberDeviceClass.WATER + ): + unit = UnitOfVolume.CUBIC_METERS + else: + unit = None + + return unit + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) host = None @@ -406,7 +462,6 @@ class ECHONETConnector: def __init__(self, instance, hass, entry): self.hass = hass self._host = instance["host"] - self._instance = None self._eojgc = instance["eojgc"] self._eojcc = instance["eojcc"] self._eojci = instance["eojci"] diff --git a/custom_components/echonetlite/climate.py b/custom_components/echonetlite/climate.py index 085faa5..2b236aa 100644 --- a/custom_components/echonetlite/climate.py +++ b/custom_components/echonetlite/climate.py @@ -1,6 +1,7 @@ import logging from pychonet.HomeAirConditioner import ( + AIRFLOW_VERT, ENL_STATUS, ENL_FANSPEED, ENL_AIR_VERT, @@ -11,6 +12,8 @@ ENL_HVAC_SET_HUMIDITY, ENL_HVAC_ROOM_TEMP, ENL_HVAC_SILENT_MODE, + FAN_SPEED, + SILENT_MODE, ) from pychonet.EchonetInstance import ENL_GETMAP @@ -32,34 +35,22 @@ from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_WHOLE, + UnitOfTemperature, ) -from .const import DOMAIN, SILENT_MODE_OPTIONS, OPTION_HA_UI_SWING +from .const import DATA_STATE_ON, DOMAIN, OPTION_HA_UI_SWING _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = 0 -DEFAULT_FAN_MODES = [ - "auto", - "minimum", - "low", - "medium-low", - "medium", - "medium-high", - "high", - "very-high", - "max", -] +DEFAULT_FAN_MODES = list( + FAN_SPEED.keys() +) # ["auto","minimum","low","medium-low","medium","medium-high","high","very-high","max"] DEFAULT_HVAC_MODES = ["heat", "cool", "dry", "fan_only", "heat_cool", "off"] -DEFAULT_SWING_MODES = [ - "auto-vert", - "upper", - "upper-central", - "central", - "lower-central", - "lower", -] -DEFAULT_PRESET_MODES = ["normal", "high-speed", "silent"] +DEFAULT_SWING_MODES = ["auto-vert"] + list( + AIRFLOW_VERT.keys() +) # ["auto-vert","upper","upper-central","central","lower-central","lower"] +DEFAULT_PRESET_MODES = list(SILENT_MODE.keys()) # ["normal", "high-speed", "silent"] SERVICE_SET_HUMIDIFER_DURING_HEATER = "set_humidifier_during_heater" ATTR_STATE = "state" @@ -73,11 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): if ( entity["instance"]["eojgc"] == 0x01 and entity["instance"]["eojcc"] == 0x30 ): # Home Air Conditioner - entities.append( - EchonetClimate( - config_entry.title, entity["echonetlite"], hass.config.units - ) - ) + entities.append(EchonetClimate(config_entry.title, entity["echonetlite"])) async_add_devices(entities, True) platform = entity_platform.async_get_current_platform() @@ -98,7 +85,12 @@ class EchonetClimate(ClimateEntity): _attr_translation_key = DOMAIN def __init__( - self, name, connector, units: UnitSystem, fan_modes=None, swing_vert=None + self, + name, + connector, + units: UnitSystem | None = None, + fan_modes=None, + swing_vert=None, ): """Initialize the climate device.""" self._name = name @@ -107,7 +99,10 @@ def __init__( self._uid = ( self._connector._uidi if self._connector._uidi else self._connector._uid ) - self._unit_of_measurement = units.temperature_unit + # The temperature unit of echonet lite is defined as Celsius. + # Set temperature_unit setting to Celsius, + # HA's automatic temperature unit conversion function works correctly. + self._unit_of_measurement = UnitOfTemperature.CELSIUS self._precision = 1.0 self._target_temperature_step = 1 self._support_flags = SUPPORT_FLAGS @@ -117,6 +112,14 @@ def __init__( self._server_state = self._connector._api._state[ self._connector._instance._host ] + self._opc_data = { + ENL_AUTO_DIRECTION: list( + self._connector._instance.EPC_FUNCTIONS[ENL_AUTO_DIRECTION][1].values() + ), + ENL_SWING_MODE: list( + self._connector._instance.EPC_FUNCTIONS[ENL_SWING_MODE][1].values() + ), + } if ENL_FANSPEED in list(self._connector._setPropertyMap): self._support_flags = self._support_flags | ClimateEntityFeature.FAN_MODE if ENL_AIR_VERT in list(self._connector._setPropertyMap): @@ -168,7 +171,7 @@ def device_info(self): "manufacturer": self._connector._manufacturer, "model": EOJX_CLASS[self._connector._instance._eojgc][ self._connector._instance._eojcc - ] + ], # "sw_version": "", } @@ -237,7 +240,7 @@ def available(self) -> bool: def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" mode = self._connector._update_data[ENL_HVAC_MODE] - if self._connector._update_data[ENL_STATUS] == "On": + if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON: if mode == "auto": mode = HVACMode.HEAT_COOL elif mode == "other": @@ -254,7 +257,7 @@ def hvac_mode(self): @property def hvac_action(self): """Return current operation ie. heat, cool, idle.""" - if self._connector._update_data[ENL_STATUS] == "On": + if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON: if self._connector._update_data[ENL_HVAC_MODE] == HVACMode.HEAT: return HVACAction.HEATING elif self._connector._update_data[ENL_HVAC_MODE] == HVACMode.COOL: @@ -300,7 +303,9 @@ def hvac_modes(self): @property def is_on(self): """Return true if the device is on.""" - return True if self._connector._update_data[ENL_STATUS] == "On" else False + return ( + True if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON else False + ) @property def fan_mode(self): @@ -369,15 +374,9 @@ def swing_mode(self): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" - if ( - self._connector._user_options.get(ENL_AUTO_DIRECTION) - and swing_mode in self._connector._user_options[ENL_AUTO_DIRECTION] - ): + if swing_mode in self._opc_data[ENL_AUTO_DIRECTION]: await self._connector._instance.setAutoDirection(swing_mode) - elif ( - self._connector._user_options.get(ENL_SWING_MODE) - and swing_mode in self._connector._user_options[ENL_SWING_MODE] - ): + elif swing_mode in self._opc_data[ENL_SWING_MODE]: await self._connector._instance.setSwingMode(swing_mode) else: await self._connector._instance.setAirflowVert(swing_mode) diff --git a/custom_components/echonetlite/config_flow.py b/custom_components/echonetlite/config_flow.py index b0fa5f7..655726d 100644 --- a/custom_components/echonetlite/config_flow.py +++ b/custom_components/echonetlite/config_flow.py @@ -1,4 +1,5 @@ """Config flow for echonetlite integration.""" + from __future__ import annotations import logging @@ -189,6 +190,7 @@ async def enumerate_instances( uidi = f"{uid}-{eojgc}-{eojcc}-{instance}" name = None if host_product_code == "WTY2001" and eojcc == 0x91: + # Panasonic WTY2001 Advanced Series Link Plus Wireless Adapter await server.echonetMessage( host, eojgc, @@ -197,9 +199,11 @@ async def enumerate_instances( GET, [{"EPC": 0xFD}, {"EPC": 0xFE}], ) - uidi = _null_padded_optional_string( - state["instances"][eojgc][eojcc][instance][0xFE] - ) + # When updating the previous environment, the entity ID will change if the uidi changes, so this should be postponed. + # Reconsider if the instance number changes dynamically. + # uidi = _null_padded_optional_string( + # state["instances"][eojgc][eojcc][instance][0xFE] + # ) name = _null_padded_optional_string( state["instances"][eojgc][eojcc][instance][0xFD] ) @@ -289,6 +293,8 @@ async def async_step_user( if len(_detected_hosts): host = list(_detected_hosts.keys()).pop(0) title = _detected_hosts[host][0]["manufacturer"] + if _detected_hosts[host][0]["host_product_code"]: + title += " " + _detected_hosts[host][0]["host_product_code"] else: if user_input is None: host = title = WORD_OF_AUTO_DISCOVERY diff --git a/custom_components/echonetlite/const.py b/custom_components/echonetlite/const.py index cd6c52f..471c87b 100644 --- a/custom_components/echonetlite/const.py +++ b/custom_components/echonetlite/const.py @@ -1,18 +1,23 @@ """Constants for the echonetlite integration.""" + from homeassistant.const import ( CONF_ICON, CONF_TYPE, - CONF_SERVICE, CONF_SERVICE_DATA, CONF_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_MINIMUM, + CONF_MAXIMUM, PERCENTAGE, - UnitOfVolume, ) from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorStateClass, SensorDeviceClass, ) +from homeassistant.components.number.const import ( + NumberDeviceClass, +) from pychonet.HomeAirConditioner import ( ENL_HVAC_MODE, ENL_FANSPEED, @@ -43,9 +48,17 @@ CONF_ICON_POSITIVE = "icon_positive" CONF_ICON_NEGATIVE = "icon_negative" CONF_ICON_ZERO = "icon_zero" -DATA_STATE_ON = "On" -DATA_STATE_OFF = "Off" +CONF_ICONS = "icons" +CONF_AS_ZERO = "as_zero" +CONF_MAX_OPC = "max_opc" +CONF_BYTE_LENGTH = "byte_len" + +DATA_STATE_ON = "on" +DATA_STATE_OFF = "off" TYPE_SWITCH = "switch" +TYPE_SELECT = "select" +TYPE_TIME = "time" +TYPE_NUMBER = "number" TYPE_DATA_DICT = "type_data_dict" TYPE_DATA_ARRAY_WITH_SIZE_OPCODE = "type_data_array_with_size_opcode" SERVICE_SET_ON_TIMER_TIME = "set_on_timer_time" @@ -71,8 +84,8 @@ COVER_SELECT_OP_CODES = {0xE0: {OPEN: 0x41, CLOSE: 0x42, STOP: 0x43}} ENL_OP_CODES = { - 0x00: { - 0x11: { + 0x00: { # Sensor-related Device + 0x11: { # Temperature sensor 0xE0: { CONF_ICON: "mdi:thermometer", CONF_TYPE: SensorDeviceClass.TEMPERATURE, @@ -80,120 +93,205 @@ }, } }, - 0x01: { - 0x30: { + 0x01: { # Air Conditioner-related Device + 0x30: { # Home air conditioner 0x84: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0x85: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, + # 0xB3: { # for develop test + # CONF_TYPE: SensorDeviceClass.TEMPERATURE, + # CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + # TYPE_NUMBER: { # Make Number input entity if settable value + # CONF_TYPE: NumberDeviceClass.TEMPERATURE, # NumberDeviceClass.x + # CONF_AS_ZERO: 0x1, # Value as zero + # CONF_MINIMUM: 0x0, # Minimum value + # CONF_MAXIMUM: 0x32, # Maximum value + # CONF_MAX_OPC: None, # OPC of max value + # CONF_BYTE_LENGTH: 0x1, # Data byte length + # TYPE_SWITCH: { # Additional switch + # CONF_NAME: "Auto", # Additionale name + # CONF_ICON: "mdi:thermometer", + # CONF_SERVICE_DATA: {DATA_STATE_ON: 23, DATA_STATE_OFF: 22}, + # }, + # }, + # }, 0xB4: { # Humidity setting in dry mode - CONF_ICON: "mdi:water-percent", CONF_TYPE: SensorDeviceClass.HUMIDITY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, - CONF_SERVICE: [SERVICE_SET_INT_1B], + TYPE_NUMBER: { + CONF_TYPE: NumberDeviceClass.HUMIDITY, + CONF_MINIMUM: 30, + CONF_MAXIMUM: 90, + }, }, 0xBA: { - CONF_ICON: "mdi:water-percent", CONF_TYPE: SensorDeviceClass.HUMIDITY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xBE: { - CONF_ICON: "mdi:thermometer", CONF_TYPE: SensorDeviceClass.TEMPERATURE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xBB: { - CONF_ICON: "mdi:thermometer", CONF_TYPE: SensorDeviceClass.TEMPERATURE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, + 0xA0: { + CONF_ICON: "mdi:fan", + }, + 0xA1: { + CONF_ICON: "mdi:shuffle-variant", + }, + 0xA3: { + CONF_ICON: "mdi:arrow-oscillating", + }, + 0xA5: { + CONF_ICON: "mdi:tailwind", + }, + 0xA4: { + CONF_ICON: "mdi:tailwind", + }, }, - 0x35: { + 0x35: { # Air cleaner 0x84: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0x85: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, + 0xA0: { + CONF_ICON: "mdi:fan", + TYPE_SELECT: FAN_SPEED, + }, }, }, - 0x02: { + 0x02: { # Housing/Facilities-related Device + 0x60: { # Electrically operated blind/shade + 0xE0: { + CONF_ICON: "mdi:roller-shade", + CONF_ICONS: { + OPEN: "mdi:roller-shade", + CLOSE: "mdi:roller-shade-closed", + STOP: "mdi:roller-shade", + }, + } + }, + 0x61: { # Electrically operated shutter + 0xE0: { + CONF_ICON: "mdi:window-shutter-open", + CONF_ICONS: { + OPEN: "mdi:window-shutter-open", + CLOSE: "mdi:window-shutter", + STOP: "mdi:window-shutter-open", + }, + } + }, + 0x62: { # Electrically operated curtain + 0xE0: { + CONF_ICON: "mdi:curtains", + CONF_ICONS: { + OPEN: "mdi:curtains", + CLOSE: "mdi:curtains-closed", + STOP: "mdi:curtains", + }, + } + }, + 0x63: { # Electrically operated rain sliding door/shutter + 0xE0: { + CONF_ICON: "mdi:door-sliding-open", + CONF_ICONS: { + OPEN: "mdi:door-sliding-open", + CLOSE: "mdi:door-sliding", + STOP: "mdi:door-sliding-open", + }, + } + }, + 0x64: { # Electrically operated gate + 0xE0: { + CONF_ICON: "mdi:boom-gate-up-outline", + CONF_ICONS: { + OPEN: "mdi:boom-gate-up-outline", + CLOSE: "mdi:boom-gate-outline", + STOP: "mdi:boom-gate-up-outline", + }, + } + }, + 0x65: { # Electrically operated window + 0xE0: { + CONF_ICON: "mdi:window-open-variant", + CONF_ICONS: { + OPEN: "mdi:window-open-variant", + CLOSE: "mdi:window-closed-variant", + STOP: "mdi:window-open-variant", + }, + } + }, + 0x66: { # Automatically operated entrance door/sliding door + 0xE0: { + CONF_ICON: "mdi:door-sliding-open", + CONF_ICONS: { + OPEN: "mdi:door-sliding-open", + CLOSE: "mdi:door-sliding", + STOP: "mdi:door-sliding-open", + }, + } + }, 0x6F: { # Electric lock 0xE0: { CONF_ICON: "mdi:lock", - CONF_SERVICE_DATA: SWITCH_BINARY_INVERT, CONF_ENSURE_ON: ENL_STATUS, - CONF_ON_VALUE: "unlock", - CONF_OFF_VALUE: "lock", - TYPE_SWITCH: True, }, 0xE1: { CONF_ICON: "mdi:lock", - CONF_SERVICE_DATA: SWITCH_BINARY_INVERT, CONF_ENSURE_ON: ENL_STATUS, - CONF_ON_VALUE: "unlock", - CONF_OFF_VALUE: "lock", - TYPE_SWITCH: True, }, 0xE6: { CONF_ICON: None, - CONF_SERVICE_DATA: SWITCH_BINARY, CONF_ENSURE_ON: ENL_STATUS, - CONF_ON_VALUE: "on", - CONF_OFF_VALUE: "off", - TYPE_SWITCH: True, }, }, 0x72: { # Hot water generator 0x90: { CONF_ICON: "mdi:timer", - CONF_SERVICE_DATA: SWITCH_BINARY, - CONF_ENSURE_ON: ENL_STATUS, - TYPE_SWITCH: True, - }, - 0xE3: { - CONF_ICON: "mdi:bathtub-outline", - CONF_SERVICE_DATA: SWITCH_BINARY, - CONF_ENSURE_ON: ENL_STATUS, - TYPE_SWITCH: True, - }, - 0xE4: { - CONF_ICON: "mdi:heat-wave", - CONF_SERVICE_DATA: SWITCH_BINARY, - CONF_ENSURE_ON: ENL_STATUS, - TYPE_SWITCH: True, }, 0x91: { # Sensor with service CONF_ICON: "mdi:timer-outline", - CONF_TYPE: None, - CONF_SERVICE: [SERVICE_SET_ON_TIMER_TIME], }, 0xD1: { # Sensor - CONF_ICON: "mdi:thermometer", CONF_TYPE: SensorDeviceClass.TEMPERATURE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, - CONF_SERVICE: [SERVICE_SET_INT_1B], + TYPE_NUMBER: { + CONF_TYPE: NumberDeviceClass.TEMPERATURE, + CONF_MINIMUM: 30, + CONF_MAXIMUM: 70, + }, }, 0xE1: { - CONF_ICON: "mdi:thermometer", CONF_TYPE: SensorDeviceClass.TEMPERATURE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, - CONF_SERVICE: [SERVICE_SET_INT_1B], + TYPE_NUMBER: { + CONF_TYPE: NumberDeviceClass.TEMPERATURE, + CONF_MINIMUM: 30, + CONF_MAXIMUM: 70, + }, + }, + 0xE3: { + CONF_ICON: "mdi:bathtub-outline", + }, + 0xE4: { + CONF_ICON: "mdi:heat-wave", }, 0xE7: {CONF_UNIT_OF_MEASUREMENT: "L"}, 0xEE: {CONF_UNIT_OF_MEASUREMENT: "L"}, }, - 0x79: { + 0x79: { # Home solar power generation 0xE0: { CONF_ICON: "mdi:solar-power-variant-outline", CONF_TYPE: SensorDeviceClass.POWER, @@ -203,123 +301,152 @@ CONF_ICON_ZERO: "mdi:solar-power-variant-outline", }, 0xE1: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xE3: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xE5: { CONF_ICON: "mdi:percent", - CONF_TYPE: PERCENTAGE, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE6: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE8: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE9: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, }, - 0x7C: { + 0x7B: { # Floor heater + 0xE0: { + CONF_TYPE: SensorDeviceClass.TEMPERATURE, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + TYPE_NUMBER: { + CONF_TYPE: NumberDeviceClass.TEMPERATURE, + CONF_MINIMUM: 16, + CONF_MAXIMUM: 40, + TYPE_SWITCH: { + CONF_NAME: "Auto", + CONF_SERVICE_DATA: {DATA_STATE_ON: 0x41, DATA_STATE_OFF: 16}, + }, + }, + }, + 0xE1: { + CONF_ICON: "mdi:thermometer", + CONF_TYPE: None, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + CONF_UNIT_OF_MEASUREMENT: "", + TYPE_NUMBER: { + CONF_AS_ZERO: 0x30, + CONF_MINIMUM: 0x31, + CONF_MAXIMUM: 0x3F, + CONF_MAX_OPC: 0xD1, + TYPE_SWITCH: { + CONF_NAME: "Auto", + CONF_SERVICE_DATA: {DATA_STATE_ON: 0x41, DATA_STATE_OFF: 0x31}, + }, + }, + }, + 0xE2: { + CONF_TYPE: SensorDeviceClass.TEMPERATURE, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + 0xE3: { + CONF_TYPE: SensorDeviceClass.TEMPERATURE, + CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + 0x90: { + CONF_ICON: "mdi:timer", + }, + 0x91: { + CONF_ICON: "mdi:timer-outline", + }, + 0x94: { + CONF_ICON: "mdi:timer", + }, + 0x95: { + CONF_ICON: "mdi:timer-outline", + }, + }, + 0x7C: { # Fuel cell 0xC2: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xC4: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xC5: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xCC: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xCD: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xC7: { - CONF_ICON: "mdi:gas-burner", CONF_TYPE: SensorDeviceClass.GAS, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, CONF_UNIT_OF_MEASUREMENT: "L/h", }, 0xC8: { - CONF_ICON: "mdi:gas-burner", CONF_TYPE: SensorDeviceClass.GAS, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "L", }, }, - 0x7D: { + 0x7D: { # Storage battery 0xA0: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA1: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA2: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA3: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA4: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA5: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xA6: { - CONF_ICON: "mdi:percent", - CONF_TYPE: PERCENTAGE, + CONF_TYPE: SensorDeviceClass.BATTERY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xA7: { - CONF_ICON: "mdi:percent", - CONF_TYPE: PERCENTAGE, + CONF_TYPE: SensorDeviceClass.BATTERY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xA8: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xA9: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, @@ -346,42 +473,34 @@ CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xD6: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xD8: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, 0xE0: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xE2: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xE4: { - CONF_ICON: "mdi:battery", - CONF_TYPE: PERCENTAGE, + CONF_TYPE: SensorDeviceClass.BATTERY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE5: { - CONF_ICON: "mdi:percent", - CONF_TYPE: PERCENTAGE, + CONF_TYPE: SensorDeviceClass.BATTERY, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE7: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, 0xE8: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL, }, @@ -396,34 +515,41 @@ CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, }, - 0x80: { + 0x80: { # Electric energy meter 0xE0: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", CONF_MULTIPLIER_OPCODE: 0xE2, - } + }, + 0xE2: { + CONF_DISABLED_DEFAULT: True, + }, }, - 0x81: { + 0x81: { # Water flow meter 0xE0: { - CONF_ICON: "mdi:water", - CONF_TYPE: UnitOfVolume.CUBIC_METERS, + # CONF_ICON: "mdi:water", + CONF_TYPE: SensorDeviceClass.WATER, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_MULTIPLIER_OPCODE: 0xE1, - } + }, + 0xE1: { + CONF_DISABLED_DEFAULT: True, + }, }, - 0x82: { + 0x82: { # Gas meter 0xE0: { - CONF_ICON: "mdi:gas-burner", + # CONF_ICON: "mdi:gas-burner", CONF_TYPE: SensorDeviceClass.GAS, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_MULTIPLIER: 0.001, } }, - 0x87: { + 0x87: { # Distribution panel metering + 0xC2: { + CONF_DISABLED_DEFAULT: True, + }, 0xB3: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", @@ -431,27 +557,23 @@ CONF_MULTIPLIER_OPCODE: 0xC2, }, 0xB7: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, TYPE_DATA_ARRAY_WITH_SIZE_OPCODE: 0xB1, }, 0xC0: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", CONF_MULTIPLIER_OPCODE: 0xC2, }, 0xC1: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", CONF_MULTIPLIER_OPCODE: 0xC2, }, 0xC6: { - CONF_ICON: "mdi:flash", CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, @@ -468,17 +590,21 @@ CONF_DISABLED_DEFAULT: True, }, }, - 0x88: { + 0x88: { # Low voltage smart electric energy meter + 0xD3: { + CONF_DISABLED_DEFAULT: True, + }, 0xE0: { - CONF_ICON: None, CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", CONF_MULTIPLIER_OPCODE: 0xE1, CONF_MULTIPLIER_OPTIONAL_OPCODE: 0xD3, }, + 0xE1: { + CONF_DISABLED_DEFAULT: True, + }, 0xE3: { - CONF_ICON: None, CONF_TYPE: SensorDeviceClass.ENERGY, CONF_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, CONF_UNIT_OF_MEASUREMENT: "kWh", @@ -486,25 +612,32 @@ CONF_MULTIPLIER_OPTIONAL_OPCODE: 0xD3, }, 0xE7: { - CONF_ICON: None, CONF_TYPE: SensorDeviceClass.POWER, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, }, 0xE8: { - CONF_ICON: None, CONF_TYPE: SensorDeviceClass.CURRENT, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, TYPE_DATA_DICT: ["r_phase_amperes", "t_phase_amperes"], }, + # 0xEA: { + # TYPE_DATA_DICT: ["time", "culmative_value"], + # }, + # 0xEB: { + # TYPE_DATA_DICT: ["time", "culmative_value"], + # }, 0xD3: {CONF_DISABLED_DEFAULT: True}, 0xE1: {CONF_DISABLED_DEFAULT: True}, }, - 0xA3: { + 0xA3: { # Lighting system 0xC0: { # Set scene CONF_ICON: "mdi:palette", CONF_TYPE: DEVICE_CLASS_ECHONETLITE_LIGHT_SCENE, CONF_STATE_CLASS: SensorStateClass.MEASUREMENT, - CONF_SERVICE: [SERVICE_SET_INT_1B], + TYPE_NUMBER: { + CONF_MAXIMUM: 0xFD, + CONF_MAX_OPC: 0xC1, + }, }, }, }, diff --git a/custom_components/echonetlite/fan.py b/custom_components/echonetlite/fan.py index 7e826f3..96a3d32 100644 --- a/custom_components/echonetlite/fan.py +++ b/custom_components/echonetlite/fan.py @@ -6,7 +6,7 @@ from homeassistant.const import ( PRECISION_WHOLE, ) -from .const import DOMAIN +from .const import DATA_STATE_ON, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -105,7 +105,7 @@ def device_info(self): "manufacturer": self._connector._manufacturer, "model": EOJX_CLASS[self._connector._instance._eojgc][ self._connector._instance._eojcc - ] + ], # "sw_version": "", } @@ -132,7 +132,7 @@ def available(self) -> bool: @property def is_on(self): """Return true if the device is on.""" - return True if self._connector._update_data[0x80] == "On" else False + return True if self._connector._update_data[0x80] == DATA_STATE_ON else False async def async_turn_on( self, diff --git a/custom_components/echonetlite/light.py b/custom_components/echonetlite/light.py index 4a37b72..a930346 100644 --- a/custom_components/echonetlite/light.py +++ b/custom_components/echonetlite/light.py @@ -12,7 +12,7 @@ COLOR_MODE_COLOR_TEMP, ) -from .const import DOMAIN, CONF_FORCE_POLLING +from .const import DATA_STATE_ON, DOMAIN, CONF_FORCE_POLLING _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices): class EchonetLight(LightEntity): - """Representation of a ECHONET light device.""" def __init__(self, name, connector): @@ -117,7 +116,7 @@ def device_info(self): "manufacturer": self._connector._manufacturer, "model": EOJX_CLASS[self._connector._instance._eojgc][ self._connector._instance._eojcc - ] + ], # "sw_version": "", } @@ -144,7 +143,9 @@ def available(self) -> bool: @property def is_on(self): """Return true if the device is on.""" - return True if self._connector._update_data[ENL_STATUS] == "On" else False + return ( + True if self._connector._update_data[ENL_STATUS] == DATA_STATE_ON else False + ) async def async_turn_on(self, **kwargs): """Turn on.""" diff --git a/custom_components/echonetlite/number.py b/custom_components/echonetlite/number.py new file mode 100644 index 0000000..e7e5b6b --- /dev/null +++ b/custom_components/echonetlite/number.py @@ -0,0 +1,179 @@ +import logging +from homeassistant.const import ( + CONF_ICON, + CONF_TYPE, + CONF_MINIMUM, + CONF_MAXIMUM, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.exceptions import InvalidStateError +from homeassistant.components.number import NumberEntity +from pychonet.lib.epc import EPC_CODE +from pychonet.lib.eojx import EOJX_CLASS +from . import get_unit_by_devise_class +from .const import ( + DOMAIN, + CONF_FORCE_POLLING, + ENL_OP_CODES, + CONF_AS_ZERO, + CONF_MAX_OPC, + CONF_BYTE_LENGTH, + TYPE_NUMBER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): + entities = [] + for entity in hass.data[DOMAIN][config.entry_id]: + eojgc = entity["instance"]["eojgc"] + eojcc = entity["instance"]["eojcc"] + _enl_op_codes = ENL_OP_CODES.get(eojgc, {}).get(eojcc, {}) + # configure select entities by looking up full ENL_OP_CODE dict + for op_code in entity["instance"]["setmap"]: + if TYPE_NUMBER in _enl_op_codes.get(op_code, {}).keys(): + entities.append( + EchonetNumber( + hass, + entity["echonetlite"], + config, + op_code, + _enl_op_codes[op_code], + entity["echonetlite"]._name or config.title, + ) + ) + + async_add_entities(entities, True) + + +class EchonetNumber(NumberEntity): + _attr_translation_key = DOMAIN + + def __init__(self, hass, connector, config, code, options, name=None): + """Initialize the number.""" + self._connector = connector + self._config = config + self._code = code + self._server_state = self._connector._api._state[ + self._connector._instance._host + ] + self._attr_icon = options.get(CONF_ICON, None) + self._attr_name = f"{config.title} {EPC_CODE[self._connector._eojgc][self._connector._eojcc][self._code]}" + self._attr_unique_id = ( + f"{self._connector._uidi}-{self._code}" + if self._connector._uidi + else f"{self._connector._uid}-{self._code}" + ) + + self._options = options[TYPE_NUMBER] + self._as_zero = int(options[TYPE_NUMBER].get(CONF_AS_ZERO, 0)) + self._conf_max = int(options[TYPE_NUMBER][CONF_MAXIMUM]) + self._byte_length = int(options[TYPE_NUMBER].get(CONF_BYTE_LENGTH, 1)) + + self._device_name = name + self._attr_device_class = self._options.get( + CONF_TYPE, options.get(CONF_TYPE, None) + ) + self._attr_should_poll = True + self._attr_available = True + self._attr_native_value = self.get_value() + self._attr_native_max_value = self.get_max_value() + self._attr_native_min_value = self._options.get(CONF_MINIMUM, 0) - self._as_zero + self._attr_native_unit_of_measurement = self._options.get( + CONF_UNIT_OF_MEASUREMENT, options.get(CONF_UNIT_OF_MEASUREMENT, None) + ) + if not self._attr_native_unit_of_measurement: + self._attr_native_unit_of_measurement = get_unit_by_devise_class( + self._attr_device_class + ) + + self.update_option_listener() + + @property + def device_info(self): + return { + "identifiers": { + ( + DOMAIN, + self._connector._uid, + self._connector._instance._eojgc, + self._connector._instance._eojcc, + self._connector._instance._eojci, + ) + }, + "name": self._device_name, + "manufacturer": self._connector._manufacturer, + "model": EOJX_CLASS[self._connector._instance._eojgc][ + self._connector._instance._eojcc + ], + # "sw_version": "", + } + + def get_value(self): + value = self._connector._update_data.get(self._code) + if value != None: + return int(self._connector._update_data.get(self._code)) - self._as_zero + else: + return None + + def get_max_value(self): + max_value = self.get_max_opc_value() + if max_value == None: + max_value = self._conf_max + return max_value - self._as_zero + + def get_max_opc_value(self): + max_opc_value = None + if self._options.get(CONF_MAX_OPC): + max_opc_value = self._connector._update_data.get(CONF_MAX_OPC) + if max_opc_value != None: + max_opc_value = int(max_opc_value) + return max_opc_value + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + if await self._connector._instance.setMessage( + self._code, int(value + self._as_zero), self._byte_length + ): + # self._connector._update_data[epc] = value + # self.async_write_ha_state() + pass + else: + raise InvalidStateError( + "The state setting is not supported or is an invalid value." + ) + + async def async_update(self): + """Retrieve latest state.""" + try: + await self._connector.async_update() + except TimeoutError: + pass + + async def async_added_to_hass(self): + """Register callbacks.""" + self._connector.add_update_option_listener(self.update_option_listener) + self._connector.register_async_update_callbacks(self.async_update_callback) + + async def async_update_callback(self, isPush=False): + new_val = self.get_value() + changed = ( + self._attr_native_value != new_val + or self._attr_available != self._server_state["available"] + or self._attr_native_max_value != self.get_max_value() + ) + if changed: + self._attr_native_value = new_val + self._attr_native_max_value = self.get_max_value() + self._attr_available = self._server_state["available"] + self.async_schedule_update_ha_state() + + def update_option_listener(self): + self._attr_should_poll = ( + self._connector._user_options.get(CONF_FORCE_POLLING, False) + or self._code not in self._connector._ntfPropertyMap + ) + _LOGGER.info( + f"{self._device_name}({self._code}): _should_poll is {self._attr_should_poll}" + ) diff --git a/custom_components/echonetlite/select.py b/custom_components/echonetlite/select.py index b7f3e7d..8669d89 100644 --- a/custom_components/echonetlite/select.py +++ b/custom_components/echonetlite/select.py @@ -1,14 +1,16 @@ import logging +from homeassistant.const import CONF_ICON from homeassistant.components.select import SelectEntity from .const import ( - HVAC_SELECT_OP_CODES, DOMAIN, - FAN_SELECT_OP_CODES, - COVER_SELECT_OP_CODES, CONF_FORCE_POLLING, + ENL_OP_CODES, + CONF_ICONS, + TYPE_SELECT, ) from pychonet.lib.epc import EPC_CODE from pychonet.lib.eojx import EOJX_CLASS +from pychonet.lib.epc_functions import _swap_dict _LOGGER = logging.getLogger(__name__) @@ -16,64 +18,31 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): entities = [] for entity in hass.data[DOMAIN][config.entry_id]: - if ( - entity["instance"]["eojgc"] == 1 and entity["instance"]["eojcc"] == 48 - ): # Home Air Conditioner - for op_code in entity["instance"]["setmap"]: - if op_code in HVAC_SELECT_OP_CODES: - entities.append( - EchonetSelect( - hass, - entity["echonetlite"], - config, - op_code, - HVAC_SELECT_OP_CODES[op_code], - entity["echonetlite"]._name or config.title, - ) - ) - elif ( - entity["instance"]["eojgc"] == 1 and entity["instance"]["eojcc"] == 53 - ): # Home Air Cleaner - for op_code in entity["instance"]["setmap"]: - if op_code in FAN_SELECT_OP_CODES: - entities.append( - EchonetSelect( - hass, - entity["echonetlite"], - config, - op_code, - FAN_SELECT_OP_CODES[op_code], - entity["echonetlite"]._name or config.title, - ) - ) - elif entity["instance"]["eojgc"] == 0x02 and entity["instance"]["eojcc"] in ( - 0x60, - 0x61, - 0x62, - 0x63, - 0x64, - 0x65, - 0x66, - ): - # 0x60: "Electrically operated blind/shade" - # 0x61: "Electrically operated shutter" - # 0x62: "Electrically operated curtain" - # 0x63: "Electrically operated rain sliding door/shutter" - # 0x64: "Electrically operated gate" - # 0x65: "Electrically operated window" - # 0x66: "Automatically operated entrance door/sliding door" - for op_code in entity["instance"]["setmap"]: - if op_code in COVER_SELECT_OP_CODES: - entities.append( - EchonetSelect( - hass, - entity["echonetlite"], - config, - op_code, - COVER_SELECT_OP_CODES[op_code], - entity["echonetlite"]._name or config.title, - ) + eojgc = entity["instance"]["eojgc"] + eojcc = entity["instance"]["eojcc"] + _enl_op_codes = ENL_OP_CODES.get(eojgc, {}).get(eojcc, {}) + # configure select entities by looking up full ENL_OP_CODE dict + for op_code in entity["instance"]["setmap"]: + epc_function_data = entity["echonetlite"]._instance.EPC_FUNCTIONS.get( + op_code, None + ) + _by_epc_func = ( + type(epc_function_data) == list + and type(epc_function_data[1]) == dict + and len(epc_function_data[1]) > 2 + ) + if _by_epc_func or TYPE_SELECT in _enl_op_codes.get(op_code, {}).keys(): + entities.append( + EchonetSelect( + hass, + entity["echonetlite"], + config, + op_code, + _enl_op_codes.get(op_code, {}), + entity["echonetlite"]._name or config.title, ) + ) + async_add_entities(entities, True) @@ -90,7 +59,15 @@ def __init__(self, hass, connector, config, code, options, name=None): self._connector._instance._host ] self._sub_state = None - self._options = options + if type(options.get(TYPE_SELECT)) == dict: + self._options = options[TYPE_SELECT] + else: + # Read from _instance.EPC FUNCTIONS definition + # Swap key, value of _instance.EPC_FUNCTIONS[opc][1] + self._options = _swap_dict(connector._instance.EPC_FUNCTIONS[code][1]) + self._icons = options.get(CONF_ICONS, {}) + self._attr_icon = options.get(CONF_ICON, None) + self._icon_default = self._attr_icon self._attr_options = list(self._options.keys()) if self._code in list(self._connector._user_options.keys()): if self._connector._user_options[code] is not False: @@ -105,6 +82,7 @@ def __init__(self, hass, connector, config, code, options, name=None): self._device_name = name self._should_poll = True self._available = True + self._attr_force_update = False self.update_option_listener() @property @@ -128,7 +106,7 @@ def device_info(self): "manufacturer": self._connector._manufacturer, "model": EOJX_CLASS[self._connector._instance._eojgc][ self._connector._instance._eojcc - ] + ], # "sw_version": "", } @@ -143,7 +121,14 @@ def available(self) -> bool: return self._available async def async_select_option(self, option: str): - await self._connector._instance.setMessage(self._code, self._options[option]) + self._attr_current_option = option + self.async_schedule_update_ha_state() + if not await self._connector._instance.setMessage( + self._code, self._options[option] + ): + # Restore previous state + self._attr_current_option = self._connector._update_data.get(self._code) + self.async_schedule_update_ha_state() async def async_update(self): """Retrieve latest state.""" @@ -161,6 +146,7 @@ def update_attr(self): ] if keys: self._attr_current_option = keys[0] + self._attr_icon = self._icons.get(self._attr_current_option, self._icon_default) if self._code in list(self._connector._user_options.keys()): if self._connector._user_options[self._code] is not False: self._attr_options = self._connector._user_options[self._code] diff --git a/custom_components/echonetlite/sensor.py b/custom_components/echonetlite/sensor.py index 33e47b3..31b582b 100644 --- a/custom_components/echonetlite/sensor.py +++ b/custom_components/echonetlite/sensor.py @@ -1,4 +1,5 @@ """Support for ECHONETLite sensors.""" + import logging import voluptuous as vol @@ -7,30 +8,27 @@ CONF_SERVICE, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, - PERCENTAGE, - UnitOfPower, - UnitOfTemperature, - UnitOfEnergy, - UnitOfVolume, - UnitOfElectricCurrent, - UnitOfElectricPotential, ) from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.helpers.typing import StateType from homeassistant.exceptions import InvalidStateError, NoEntitySpecifiedError from pychonet.GeneralLighting import ENL_BRIGHTNESS, ENL_COLOR_TEMP from pychonet.lib.epc import EPC_CODE, EPC_SUPER from pychonet.lib.eojx import EOJX_CLASS -from pychonet.ElectricBlind import ENL_OPENSTATE +from pychonet.lib.epc_functions import _hh_mm + +from . import get_unit_by_devise_class from .const import ( DOMAIN, ENL_OP_CODES, CONF_STATE_CLASS, TYPE_SWITCH, + TYPE_SELECT, + TYPE_TIME, + TYPE_NUMBER, SERVICE_SET_ON_TIMER_TIME, SERVICE_SET_INT_1B, ENL_STATUS, @@ -49,6 +47,19 @@ _LOGGER = logging.getLogger(__name__) +# +def is_regist_as_sensor(epc_function_data): + if epc_function_data: + if type(epc_function_data) == list: + if type(epc_function_data[1]) == dict and len(epc_function_data[1]) > 1: + return True # Switch or Select + if callable(epc_function_data[0]) and epc_function_data[0] == _hh_mm: + return True # Time + elif callable(epc_function_data) and epc_function_data == _hh_mm: + return True # Time + return False + + async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): entities = [] platform = entity_platform.async_get_current_platform() @@ -60,20 +71,33 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non eojgc = entity["instance"]["eojgc"] eojcc = entity["instance"]["eojcc"] power_switch = ENL_STATUS in entity["instance"]["setmap"] - mode_select = ENL_OPENSTATE in entity["instance"]["setmap"] - + # mode_select = ENL_OPENSTATE in entity["instance"]["setmap"] + _enl_op_codes = ENL_OP_CODES.get(eojgc, {}).get(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 not all sensors will be configured." ) - for op_code in ENL_OP_CODES[eojgc][eojcc].keys(): + for op_code in _enl_op_codes.keys(): + epc_function_data = entity["echonetlite"]._instance.EPC_FUNCTIONS.get( + op_code, None + ) if op_code in entity["instance"]["getmap"]: + _keys = _enl_op_codes.get(op_code, {}).keys() + if ( + TYPE_SWITCH in _keys + or TYPE_SELECT in _keys + or TYPE_TIME in _keys + or TYPE_NUMBER in _keys + or is_regist_as_sensor(epc_function_data) + ): + if op_code in entity["instance"]["setmap"]: + continue entities.append( EchonetSensor( entity["echonetlite"], op_code, - ENL_OP_CODES[eojgc][eojcc][op_code], + _enl_op_codes.get(op_code, {}), entity["echonetlite"]._name or config.title, hass, ) @@ -82,121 +106,134 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non _LOGGER.debug( "This is an ECHONET fan device so not all sensors will be configured." ) - for op_code in ENL_OP_CODES[eojgc][eojcc].keys(): + for op_code in _enl_op_codes.keys(): if op_code in entity["instance"]["getmap"]: + _keys = _enl_op_codes.grt(op_code, {}).keys() + epc_function_data = entity[ + "echonetlite" + ]._instance.EPC_FUNCTIONS.get(op_code, None) + if ( + TYPE_SWITCH in _keys + or TYPE_SELECT in _keys + or TYPE_TIME in _keys + or TYPE_NUMBER in _keys + or is_regist_as_sensor(epc_function_data) + ): + if op_code in entity["instance"]["setmap"]: + continue entities.append( EchonetSensor( entity["echonetlite"], op_code, - ENL_OP_CODES[eojgc][eojcc][op_code], + _enl_op_codes.get(op_code, {}), entity["echonetlite"]._name or config.title, hass, ) ) else: # For all other devices, sensors will be configured but customise if applicable. for op_code in list(entity["echonetlite"]._update_flags_full_list): - if (power_switch and ENL_STATUS == op_code) or ( - mode_select and ENL_OPENSTATE == op_code - ): + # if (power_switch and ENL_STATUS == op_code) or ( + # mode_select and ENL_OPENSTATE == op_code + # ): + if power_switch and ENL_STATUS == op_code: continue if eojgc == 0x02 and (eojcc == 0x90 or eojcc == 0x91): # General Lighting, Single Function Lighting: skip already handled values if op_code == ENL_BRIGHTNESS or op_code == ENL_COLOR_TEMP: continue - if eojgc in ENL_OP_CODES.keys(): - if eojcc in ENL_OP_CODES[eojgc].keys(): - if op_code in ENL_OP_CODES[eojgc][eojcc].keys(): - _keys = ENL_OP_CODES[eojgc][eojcc][op_code].keys() - if ( - CONF_SERVICE in _keys - and op_code in entity["instance"]["setmap"] - ): # Some devices support advanced service calls. - for service_name in ENL_OP_CODES[eojgc][eojcc][op_code][ - CONF_SERVICE - ]: - if service_name == SERVICE_SET_ON_TIMER_TIME: - platform.async_register_entity_service( - service_name, - { - vol.Required( - "timer_time" - ): cv.time_period - }, - "async_" + service_name, - ) - elif service_name == SERVICE_SET_INT_1B: - platform.async_register_entity_service( - service_name, - { - vol.Required("value"): cv.positive_int, - vol.Optional( - "epc", default=op_code - ): cv.positive_int, - }, - "async_" + service_name, - ) - - if TYPE_SWITCH in _keys: - continue # dont configure as sensor, it will be configured as switch instead. - - if TYPE_DATA_DICT in _keys: - type_data = ENL_OP_CODES[eojgc][eojcc][op_code][ - TYPE_DATA_DICT - ] - if isinstance(type_data, list): - for attr_key in type_data: - attr = ENL_OP_CODES[eojgc][eojcc][ - op_code - ].copy() - attr["dict_key"] = attr_key - entities.append( - EchonetSensor( - entity["echonetlite"], - op_code, - attr, - entity["echonetlite"]._name - or config.title, - hass, - ) - ) - continue - else: - continue - if TYPE_DATA_ARRAY_WITH_SIZE_OPCODE in _keys: - array_size_op_code = ENL_OP_CODES[eojgc][eojcc][ - op_code - ][TYPE_DATA_ARRAY_WITH_SIZE_OPCODE] - array_max_size = await entity[ - "echonetlite" - ]._instance.update(array_size_op_code) - for x in range(0, array_max_size): - attr = ENL_OP_CODES[eojgc][eojcc][op_code].copy() - attr["accessor_index"] = x - attr["accessor_lambda"] = ( - lambda value, index: value["values"][index] - if index < value["range"] - else None - ) - entities.append( - EchonetSensor( - entity["echonetlite"], - op_code, - attr, - config.title, - ) - ) - continue - else: + if op_code in _enl_op_codes.keys(): + _keys = _enl_op_codes.get(op_code, {}).keys() + epc_function_data = entity[ + "echonetlite" + ]._instance.EPC_FUNCTIONS.get(op_code, None) + if ( + TYPE_SWITCH in _keys + or TYPE_SELECT in _keys + or TYPE_TIME in _keys + or TYPE_NUMBER in _keys + or is_regist_as_sensor(epc_function_data) + ): + if op_code in entity["instance"]["setmap"]: + continue # dont configure as sensor, it will be configured as switch instead. + + if ( + CONF_SERVICE in _keys + and op_code in entity["instance"]["setmap"] + ): # Some devices support advanced service calls. + for service_name in _enl_op_codes.get(op_code, {}).get( + CONF_SERVICE + ): + if service_name == SERVICE_SET_ON_TIMER_TIME: + platform.async_register_entity_service( + service_name, + {vol.Required("timer_time"): cv.time_period}, + "async_" + service_name, + ) + elif service_name == SERVICE_SET_INT_1B: + platform.async_register_entity_service( + service_name, + { + vol.Required("value"): cv.positive_int, + vol.Optional( + "epc", default=op_code + ): cv.positive_int, + }, + "async_" + service_name, + ) + + if TYPE_DATA_DICT in _keys: + type_data = _enl_op_codes.get(op_code, {}).get(TYPE_DATA_DICT) + if isinstance(type_data, list): + for attr_key in type_data: + attr = _enl_op_codes.get(op_code).copy() + attr["dict_key"] = attr_key entities.append( EchonetSensor( entity["echonetlite"], op_code, - ENL_OP_CODES[eojgc][eojcc][op_code], + attr, entity["echonetlite"]._name or config.title, hass, ) ) continue + else: + continue + if TYPE_DATA_ARRAY_WITH_SIZE_OPCODE in _keys: + array_size_op_code = _enl_op_codes[op_code][ + TYPE_DATA_ARRAY_WITH_SIZE_OPCODE + ] + array_max_size = await entity["echonetlite"]._instance.update( + array_size_op_code + ) + for x in range(0, array_max_size): + attr = _enl_op_codes[op_code].copy() + attr["accessor_index"] = x + attr["accessor_lambda"] = lambda value, index: ( + value["values"][index] + if index < value["range"] + else None + ) + entities.append( + EchonetSensor( + entity["echonetlite"], + op_code, + attr, + config.title, + ) + ) + continue + else: + entities.append( + EchonetSensor( + entity["echonetlite"], + op_code, + _enl_op_codes[op_code], + entity["echonetlite"]._name or config.title, + hass, + ) + ) + continue entities.append( EchonetSensor( entity["echonetlite"], @@ -262,30 +299,13 @@ def __init__(self, connector, op_code, attributes, name=None, hass=None) -> None self._uid += f'-{self._sensor_attributes["accessor_index"]}' self._name += f' {str(self._sensor_attributes["accessor_index"] + 1).zfill(len(str(self._sensor_attributes[TYPE_DATA_ARRAY_WITH_SIZE_OPCODE])))}' - if CONF_UNIT_OF_MEASUREMENT in _attr_keys: - self._unit_of_measurement = self._sensor_attributes[ - CONF_UNIT_OF_MEASUREMENT - ] - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.TEMPERATURE: - self._unit_of_measurement = UnitOfTemperature.CELSIUS - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.ENERGY: - self._unit_of_measurement = UnitOfEnergy.WATT_HOUR - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.POWER: - self._unit_of_measurement = UnitOfPower.WATT - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.CURRENT: - self._unit_of_measurement = UnitOfElectricCurrent.AMPERE - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.VOLTAGE: - self._unit_of_measurement = UnitOfElectricPotential.VOLT - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.HUMIDITY: - self._unit_of_measurement = PERCENTAGE - elif self._sensor_attributes[CONF_TYPE] == PERCENTAGE: - self._unit_of_measurement = PERCENTAGE - elif self._sensor_attributes[CONF_TYPE] == SensorDeviceClass.GAS: - self._unit_of_measurement = UnitOfVolume.CUBIC_METERS - elif self._sensor_attributes[CONF_TYPE] == UnitOfVolume.CUBIC_METERS: - self._unit_of_measurement = UnitOfVolume.CUBIC_METERS - else: - self._unit_of_measurement = None + self._unit_of_measurement = self._sensor_attributes.get( + CONF_UNIT_OF_MEASUREMENT + ) + if not self._unit_of_measurement: + self._unit_of_measurement = get_unit_by_devise_class( + self._sensor_attributes[CONF_TYPE] + ) self.update_option_listener() @@ -322,7 +342,7 @@ def device_info(self): }, "name": self._device_name, "manufacturer": self._connector._manufacturer, - "model": EOJX_CLASS[self._eojgc][self._eojcc] + "model": EOJX_CLASS[self._eojgc][self._eojcc], # "sw_version": "", } @@ -379,7 +399,7 @@ def get_attr_native_value(self): new_val * self._connector._update_data[multiplier_opcode] ) else: - return STATE_UNAVAILABLE + return None if CONF_MULTIPLIER_OPTIONAL_OPCODE in self._sensor_attributes: multiplier_opcode = self._sensor_attributes[ CONF_MULTIPLIER_OPTIONAL_OPCODE diff --git a/custom_components/echonetlite/switch.py b/custom_components/echonetlite/switch.py index 1e4d80c..866be5b 100644 --- a/custom_components/echonetlite/switch.py +++ b/custom_components/echonetlite/switch.py @@ -1,6 +1,6 @@ import asyncio import logging -from homeassistant.const import CONF_ICON, CONF_SERVICE_DATA +from homeassistant.const import CONF_ICON, CONF_SERVICE_DATA, CONF_NAME from homeassistant.components.switch import SwitchEntity from .const import ( DOMAIN, @@ -12,14 +12,12 @@ SWITCH_POWER, CONF_ENSURE_ON, TYPE_SWITCH, + TYPE_NUMBER, ENL_STATUS, - ENL_ON, - ENL_OFF, CONF_FORCE_POLLING, ) from pychonet.lib.epc import EPC_CODE from pychonet.lib.eojx import EOJX_CLASS -from pychonet.lib.const import ENL_SETMAP _LOGGER = logging.getLogger(__name__) @@ -30,24 +28,46 @@ async def async_setup_entry(hass, config, async_add_entities, discovery_info=Non eojgc = entity["instance"]["eojgc"] eojcc = entity["instance"]["eojcc"] set_enl_status = False + _enl_op_codes = ENL_OP_CODES.get(eojgc, {}).get(eojcc, {}) # configure switch entities by looking up full ENL_OP_CODE dict - for op_code in list(entity["echonetlite"]._update_flags_full_list): - if eojgc in ENL_OP_CODES.keys(): - if eojcc in ENL_OP_CODES[eojgc].keys(): - if op_code in ENL_OP_CODES[eojgc][eojcc].keys(): - if TYPE_SWITCH in ENL_OP_CODES[eojgc][eojcc][op_code].keys(): - entities.append( - EchonetSwitch( - hass, - entity["echonetlite"], - config, - op_code, - ENL_OP_CODES[eojgc][eojcc][op_code], - entity["echonetlite"]._name or config.title, - ) - ) - if op_code == ENL_STATUS: - set_enl_status = True + for op_code in entity["instance"]["setmap"]: + epc_function_data = entity["echonetlite"]._instance.EPC_FUNCTIONS.get( + op_code, None + ) + _by_epc_func = ( + type(epc_function_data) == list + and type(epc_function_data[1]) == dict + and len(epc_function_data[1]) == 2 + ) + if _by_epc_func or TYPE_SWITCH in _enl_op_codes.get(op_code, {}).keys(): + entities.append( + EchonetSwitch( + hass, + entity["echonetlite"], + config, + op_code, + ENL_OP_CODES[eojgc][eojcc][op_code], + entity["echonetlite"]._name or config.title, + ) + ) + if op_code == ENL_STATUS: + set_enl_status = True + if ( + switch_conf := _enl_op_codes.get(op_code, {}) + .get(TYPE_NUMBER, {}) + .get(TYPE_SWITCH) + ): + switch_conf.update(_enl_op_codes[op_code].copy()) + entities.append( + EchonetSwitch( + hass, + entity["echonetlite"], + config, + op_code, + switch_conf, + entity["echonetlite"]._name or config.title, + ) + ) # Auto configure of the power switch if (eojgc == 0x01 and eojcc in (0x30, 0x35)) or ( eojgc == 0x02 and eojcc in (0x90, 0x91) @@ -78,19 +98,37 @@ def __init__(self, hass, connector, config, code, options, name=None): self._config = config self._code = code self._options = options - self._on_value = options.get(CONF_ON_VALUE, DATA_STATE_ON) + epc_function_data = connector._instance.EPC_FUNCTIONS.get(code, None) + if type(epc_function_data) == list: + data_keys = list(epc_function_data[1].keys()) + data_items = list(epc_function_data[1].values()) + self._options.update( + { + CONF_SERVICE_DATA: { + DATA_STATE_ON: data_keys[0], + DATA_STATE_OFF: data_keys[1], + }, + CONF_ON_VALUE: data_items[0], + CONF_OFF_VALUE: data_items[1], + } + ) + self._on_value = self._options.get(CONF_ON_VALUE, DATA_STATE_ON) self._on_vals = [ self._on_value, self._options[CONF_SERVICE_DATA][DATA_STATE_ON], hex(self._options[CONF_SERVICE_DATA][DATA_STATE_ON])[2:], ] + self._from_number = True if options.get(TYPE_NUMBER) else False self._attr_name = f"{config.title} {EPC_CODE[self._connector._eojgc][self._connector._eojcc][self._code]}" - self._attr_icon = options[CONF_ICON] + self._attr_icon = options.get(CONF_ICON) self._uid = ( f"{self._connector._uidi}-{self._code}" if self._connector._uidi else f"{self._connector._uid}-{self._connector._eojgc}-{self._connector._eojcc}-{self._connector._eojci}-{self._code}" ) + if self._from_number: + self._uid += "-switch" + self._attr_name += " " + options.get(CONF_NAME, "Switch") self._device_name = name self._should_poll = True self._server_state = self._connector._api._state[ diff --git a/custom_components/echonetlite/time.py b/custom_components/echonetlite/time.py new file mode 100644 index 0000000..0fbf458 --- /dev/null +++ b/custom_components/echonetlite/time.py @@ -0,0 +1,145 @@ +import logging +import datetime +from datetime import time +from homeassistant.const import CONF_ICON +from homeassistant.components.time import TimeEntity +from .const import ( + DOMAIN, + CONF_FORCE_POLLING, + ENL_OP_CODES, + TYPE_TIME, +) +from pychonet.lib.epc import EPC_CODE +from pychonet.lib.eojx import EOJX_CLASS +from pychonet.lib.epc_functions import _hh_mm + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): + entities = [] + for entity in hass.data[DOMAIN][config.entry_id]: + eojgc = entity["instance"]["eojgc"] + eojcc = entity["instance"]["eojcc"] + _enl_op_codes = ENL_OP_CODES.get(eojgc, {}).get(eojcc, {}) + # configure select entities by looking up full ENL_OP_CODE dict + for op_code in entity["instance"]["setmap"]: + epc_function_data = entity["echonetlite"]._instance.EPC_FUNCTIONS.get( + op_code, None + ) + _by_epc_func = ( + type(epc_function_data) == list and epc_function_data[0] == _hh_mm + ) or (callable(epc_function_data) and epc_function_data == _hh_mm) + if _by_epc_func or TYPE_TIME in _enl_op_codes.get(op_code, {}).keys(): + entities.append( + EchonetTime( + hass, + entity["echonetlite"], + config, + op_code, + _enl_op_codes.get(op_code, {}), + entity["echonetlite"]._name or config.title, + ) + ) + + async_add_entities(entities, True) + + +class EchonetTime(TimeEntity): + _attr_translation_key = DOMAIN + + def __init__(self, hass, connector, config, code, options, device_name=None): + """Initialize the time.""" + self._connector = connector + self._config = config + self._code = code + self._server_state = self._connector._api._state[ + self._connector._instance._host + ] + self._attr_icon = options.get(CONF_ICON, None) + self._attr_name = f"{config.title} {EPC_CODE[self._connector._eojgc][self._connector._eojcc][self._code]}" + self._attr_unique_id = ( + f"{self._connector._uidi}-{self._code}" + if self._connector._uidi + else f"{self._connector._uid}-{self._code}" + ) + + self._device_name = device_name + self._attr_should_poll = True + self._attr_available = True + self._attr_native_value = self.get_time() + + self.update_option_listener() + + @property + def device_info(self): + return { + "identifiers": { + ( + DOMAIN, + self._connector._uid, + self._connector._instance._eojgc, + self._connector._instance._eojcc, + self._connector._instance._eojci, + ) + }, + "name": self._device_name, + "manufacturer": self._connector._manufacturer, + "model": EOJX_CLASS[self._connector._instance._eojgc][ + self._connector._instance._eojcc + ], + # "sw_version": "", + } + + def get_time(self): + hh_mm = self._connector._update_data.get(self._code) + if hh_mm != None: + val = hh_mm.split(":") + time_obj = datetime.time(int(val[0]), int(val[1])) + else: + time_obj = None + return time_obj + + async def async_set_value(self, value: time) -> None: + """Update the current value.""" + h = int(value.hour) + m = int(value.minute) + mes = {"EPC": self._code, "PDC": 0x02, "EDT": h * 256 + m} + if await self._connector._instance.setMessages([mes]): + pass + else: + raise InvalidStateError( + "The state setting is not supported or is an invalid value." + ) + + async def async_update(self): + """Retrieve latest state.""" + try: + await self._connector.async_update() + except TimeoutError: + pass + + async def async_added_to_hass(self): + """Register callbacks.""" + self._connector.add_update_option_listener(self.update_option_listener) + self._connector.register_async_update_callbacks(self.async_update_callback) + + async def async_update_callback(self, isPush=False): + new_val = self.get_time() + changed = ( + self._attr_native_value != new_val + or self._attr_available != self._server_state["available"] + ) + if changed: + self._attr_native_value = new_val + self._attr_available = self._server_state["available"] + self.async_schedule_update_ha_state() + + def update_option_listener(self): + self._attr_should_poll = ( + self._connector._user_options.get(CONF_FORCE_POLLING, False) + or self._code not in self._connector._ntfPropertyMap + ) + _LOGGER.info( + f"{self._device_name}({self._code}): _should_poll is {self._attr_should_poll}" + ) diff --git a/custom_components/echonetlite/translations/en.json b/custom_components/echonetlite/translations/en.json index b4cdd0e..3740b32 100644 --- a/custom_components/echonetlite/translations/en.json +++ b/custom_components/echonetlite/translations/en.json @@ -62,6 +62,17 @@ } }, "entity": { + "sensor": { + "echonetlite": { + "state": { + "stopped": "Stopped", + "supplying hot water": "Supplying Hot Water", + "keeping bath temperature": "Keeping Bath Temperature", + "heating": "Heating", + "not heating": "Not Heating" + } + } + }, "select": { "echonetlite": { "state": { diff --git a/custom_components/echonetlite/translations/ja.json b/custom_components/echonetlite/translations/ja.json index 86c931d..a85bc6a 100644 --- a/custom_components/echonetlite/translations/ja.json +++ b/custom_components/echonetlite/translations/ja.json @@ -65,13 +65,11 @@ "sensor": { "echonetlite": { "state": { - "Off": "オフ", - "On": "オン", "Stopped": "停止", "Supplying Hot Water": "湯はり中", "Keeping Bath Temperature": "保温中", - "Heating": "加温中", - "Not Heating": "停止" + "heating": "加温中", + "not heating": "停止" } } },