diff --git a/README.md b/README.md index dcf4fb0..cea37f5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # ConnectLife + ConnectLife integration for Home Assistant ## Installation @@ -16,17 +17,40 @@ Download the `connectlife` directory and place in your `/custom_componen After installing, you need to restart Home Assistant. -Finally add "ConnectLife" as an integration in the UI, and provide the username and password for your ConnectLife account. +Finally, add "ConnectLife" as an integration in the UI, and provide the username and password for your ConnectLife account. + +You device and all their status values should show up. + +## Supported devices + +Any unknown device will show up as sensors with names based on their properties. As there are a lot of exposed +sensors, all unknown sensors are hidden by default. Access the device or entity list to view sensors and change +visibility. -You appliances and all their status values should show up. +### Known devices + +| Device name | Device type | Device type code | Device feature code | Data dictionary | +|--------------|-----------------|------------------|---------------------|-------------------------------------------------------------------------------------------| +| W-DW50/60-22 | Dishwasher | 015 | 000 | [Most properties completed](custom_components/connectlife/data_dictionaries/015-000.yaml) | +| | Air conditioner | 009 | 109 | [In progress](custom_components/connectlife/data_dictionaries/009-109.yaml) | + +Please, please, please contribute PRs with [data dictionaries](custom_components/connectlife/data_dictionaries) for your devices! ## Issues -### Missing data dictionaries +### Climate entities + +Please ignore the following warning in the log: +``` +Entity None () implements HVACMode(s): auto, off and therefore implicitly supports the turn_on/turn_off methods without setting the proper ClimateEntityFeature. Please report it to the author of the 'connectlife' custom integration +``` -Please contribute PR with [data dictionary](custom_components/connectlife/data_dictionaries) for your device! +Missing features: +- Preset modes (e.g. eco) +- Setting HVAC mode except to off/auto +- Min/max temperatures -### Experimental service to set property values +### Experimental service to set property values (sensors only) Entity service `connectlife.set_value` can be used to set values. Use with caution, as there is **no** validation if property is writeable, or that the value is legal to set. diff --git a/custom_components/connectlife/__init__.py b/custom_components/connectlife/__init__.py index ff10c5f..da79b09 100644 --- a/custom_components/connectlife/__init__.py +++ b/custom_components/connectlife/__init__.py @@ -10,7 +10,7 @@ from .const import DOMAIN from .coordinator import ConnectLifeCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR, Platform.SELECT, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/connectlife/binary_sensor.py b/custom_components/connectlife/binary_sensor.py index f16d576..06f83aa 100644 --- a/custom_components/connectlife/binary_sensor.py +++ b/custom_components/connectlife/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,7 +31,7 @@ async def async_setup_entry( dictionary = Dictionaries.get_dictionary(appliance) async_add_entities( ConnectLifeBinaryStatusSensor(coordinator, appliance, s, dictionary[s]) - for s in appliance.status_list if hasattr(dictionary[s], "binary_sensor") + for s in appliance.status_list if hasattr(dictionary[s], Platform.BINARY_SENSOR) ) @@ -68,11 +69,5 @@ def update_state(self): self._attr_is_on = VALUES[value] else: self._attr_is_on = None - _LOGGER.warning("Unknown value %n for %s", value, self.status) + _LOGGER.warning("Unknown value %d for %s", value, self.status) self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1 - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self.update_state() - self.async_write_ha_state() diff --git a/custom_components/connectlife/climate.py b/custom_components/connectlife/climate.py new file mode 100644 index 0000000..0f8a84d --- /dev/null +++ b/custom_components/connectlife/climate.py @@ -0,0 +1,212 @@ +"""Provides climate entities for ConnectLife.""" +import logging + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, Platform, PRECISION_WHOLE, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DOMAIN, + FAN_MODE, + HVAC_ACTION, + IS_ON, + SWING_MODE, + TARGET_TEMPERATURE, + TEMPERATURE_UNIT, +) +from .coordinator import ConnectLifeCoordinator +from .dictionaries import Dictionaries, Property, Climate +from .entity import ConnectLifeEntity +from connectlife.appliance import ConnectLifeAppliance + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ConnectLife sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for appliance in coordinator.appliances.values(): + dictionary = Dictionaries.get_dictionary(appliance) + if is_climate(dictionary): + entities.append(ConnectLifeClimate(coordinator, appliance, dictionary)) + async_add_entities(entities) + + +def is_climate(dictionary: dict[str, Property]): + for property in dictionary.values(): + if hasattr(property, Platform.CLIMATE): + return True + return False + + +class ConnectLifeClimate(ConnectLifeEntity, ClimateEntity): + """Climate class for ConnectLife.""" + + _attr_has_entity_name = True + _attr_precision = PRECISION_WHOLE + _attr_target_temperature_step = 1 + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_hvac_mode = None + _attr_fan_mode = None + _attr_swing_mode = None + target_map = {} + fan_mode_map: dict[int, str] = {} + fan_mode_reverse_map: dict[str, int] = {} + hvac_action_map: dict[int, HVACMode] = {} + swing_mode_map: dict[int, str] = {} + swing_mode_reverse_map: dict[str, int] = {} + temperature_unit_map: dict[int, UnitOfTemperature] = {} + + def __init__( + self, + coordinator: ConnectLifeCoordinator, + appliance: ConnectLifeAppliance, + data_dictionary: dict[str, Property] + ): + """Initialize the entity.""" + super().__init__(coordinator, appliance) + self._attr_unique_id = f"{appliance.device_id}-climate" + + self.entity_description = ClimateEntityDescription( + key=self._attr_unique_id, + name=appliance.device_nickname, + translation_key=DOMAIN + ) + + for dd_entry in data_dictionary.values(): + if hasattr(dd_entry, Platform.CLIMATE): + self.target_map[dd_entry.climate.target] = dd_entry.name + + hvac_modes = [HVACMode.AUTO] + for target, status in self.target_map.items(): + if target == IS_ON: + self._attr_supported_features |= ClimateEntityFeature.TURN_OFF + self._attr_supported_features |= ClimateEntityFeature.TURN_ON + hvac_modes.append(HVACMode.OFF) + if target == TARGET_TEMPERATURE: + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + if target == TEMPERATURE_UNIT: + for k, v in data_dictionary[status].climate.options.items(): + if v == "celsius" or v == "C": + self.temperature_unit_map[k] = UnitOfTemperature.CELSIUS + elif v == "fahrenheit" or v == "F": + self.temperature_unit_map[k] = UnitOfTemperature.FAHRENHEIT + if target == FAN_MODE: + self.fan_mode_map = data_dictionary[status].climate.options + self.fan_mode_reverse_map = {v: k for k, v in self.fan_mode_map.items()} + self._attr_fan_modes = list(self.fan_mode_map.values()) + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + if target == SWING_MODE: + self.swing_mode_map = data_dictionary[status].climate.options + self.swing_mode_reverse_map = {v: k for k, v in self.swing_mode_map.items()} + self._attr_swing_modes = list(self.swing_mode_map.values()) + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + if target == HVAC_ACTION: + # values = set(item.value for item in Fruit) + actions = set(action.value for action in HVACAction) + for (k, v) in data_dictionary[status].climate.options.items(): + if v in actions: + self.hvac_action_map[k] = HVACAction(v) + else: + _LOGGER.warning("Not mapping %d to unknown HVACAction %s", k, v) + + self._attr_hvac_modes = hvac_modes + self.update_state() + + @callback + def update_state(self) -> None: + for target, status in self.target_map.items(): + if status in self.coordinator.appliances[self.device_id].status_list: + value = self.coordinator.appliances[self.device_id].status_list[status] + if target == IS_ON: + # TODO: Support value mapping + if value == 0: + self._attr_hvac_mode = HVACMode.OFF + else: + # TODO: Support other modes + self._attr_hvac_mode = HVACMode.AUTO + elif target == HVAC_ACTION: + if value in self.hvac_action_map: + self._attr_hvac_action = self.hvac_action_map[value] + else: + # Map to None as we canot add custom HVAC actions. + self._attr_hvac_action = None + elif target == FAN_MODE: + if value in self.fan_mode_map: + self._attr_fan_mode = self.fan_mode_map[value] + else: + self._attr_fan_mode = None + _LOGGER.warning("Got unexpected value %d for %s", value, status) + elif target == SWING_MODE: + if value in self.swing_mode_map: + self._attr_swing_mode = self.swing_mode_map[value] + else: + self._attr_swing_mode = None + _LOGGER.warning("Got unexpected value %d for %s", value, status) + elif target == TEMPERATURE_UNIT: + if value in self.temperature_unit_map: + self._attr_temperature_unit = self.temperature_unit_map[value] + else: + _LOGGER.warning("Got unexpected value %d for %s", value, status) + else: + setattr(self, f"_attr_{target}", value) + self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1 + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs and TARGET_TEMPERATURE in self.target_map: + target_temperature = round(kwargs[ATTR_TEMPERATURE]) + await self.coordinator.api.update_appliance(self.puid, {self.target_map[TARGET_TEMPERATURE]: target_temperature}) + self._attr_target_temperature = target_temperature + self.async_write_ha_state() + + async def async_turn_on(self): + """Turn the entity on.""" + # TODO: Support value mapping + await self.coordinator.api.update_appliance(self.puid, {self.target_map[IS_ON]: 1}) + self.hvac_mode = HVACMode.AUTO + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn the entity off.""" + # TODO: Support value mapping + await self.coordinator.api.update_appliance(self.puid, {self.target_map[IS_ON]: 0}) + self.hvac_mode = HVACMode.OFF + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the HVAC mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + elif hvac_mode == HVACMode.AUTO: + await self.async_turn_on() + # self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set the fan mode.""" + await self.coordinator.api.update_appliance(self.puid, { + self.target_map[FAN_MODE]: self.fan_mode_reverse_map[fan_mode] + }) + self._attr_fan_mode = fan_mode + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set the swing mode.""" + await self.coordinator.api.update_appliance(self.puid, { + self.target_map[SWING_MODE]: self.swing_mode_reverse_map[swing_mode] + }) + self._attr_swing_mode = swing_mode + self.async_write_ha_state() diff --git a/custom_components/connectlife/const.py b/custom_components/connectlife/const.py index c7e72a1..9eb8770 100644 --- a/custom_components/connectlife/const.py +++ b/custom_components/connectlife/const.py @@ -5,3 +5,10 @@ ATTR_DEVICE = "device" ATTR_DESC = "desc" ATTR_KEY = "key" + +HVAC_ACTION = "hvac_action" +FAN_MODE = "fan_mode" +IS_ON = "is_on" +SWING_MODE = "swing_mode" +TARGET_TEMPERATURE = "target_temperature" +TEMPERATURE_UNIT = "temperature_unit" diff --git a/custom_components/connectlife/data_dictionaries/009-109.yaml b/custom_components/connectlife/data_dictionaries/009-109.yaml new file mode 100644 index 0000000..a279b4f --- /dev/null +++ b/custom_components/connectlife/data_dictionaries/009-109.yaml @@ -0,0 +1,73 @@ +device_type: aircondition +properties: + - property: f_temp_in + climate: + target: current_temperature + - property: oem_host_version + sensor: + hide: true + - property: t_beep + switch: + - property: t_eco + switch: + - property: t_fan_mute + switch: + - property: t_fan_speed + climate: + target: fan_mode + options: + 0: auto + 5: low + 6: middle_low + 7: medium + 8: middle_high + 9: high + - property: t_humidity + - property: t_power + climate: + target: is_on + - property: t_sleep + - property: t_super + switch: + device_class: switch + - property: t_swing_angle + select: + options: + 0: swing + 1: auto + 2: angle_1 + 3: angle_2 + 4: angle_3 + 5: angle_4 + 6: angle_5 + 7: angle_6 + - property: t_swing_direction + climate: + target: swing_mode + options: + 0: forward + 1: right + 2: swing + 3: both_sides + 4: left + - property: t_temp + climate: + target: target_temperature + - property: t_temp_type + climate: + target: temperature_unit + options: + 0: celsius + 1: fahrenheit + - property: t_tms + switch: + device_class: switch + - property: t_work_mode + climate: + target: hvac_action + options: + 0: fan + 1: heating + 2: cooling + 3: drying + # 4: auto - not supported in Home Assistant diff --git a/custom_components/connectlife/data_dictionaries/015-000.yaml b/custom_components/connectlife/data_dictionaries/015-000.yaml index b608372..60021fa 100644 --- a/custom_components/connectlife/data_dictionaries/015-000.yaml +++ b/custom_components/connectlife/data_dictionaries/015-000.yaml @@ -1,5 +1,7 @@ +device_type: dishwasher properties: - property: ADO_allowed + hide: true sensor: writable: false - property: Alarm_auto_dose_refill @@ -7,18 +9,22 @@ properties: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_Autodose_level10 + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_Autodose_level20 + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_External_autodose_level15 + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_External_autodose_level30 + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert @@ -27,14 +33,17 @@ properties: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_door_closed + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_door_opened + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_preheating_ready + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert @@ -43,10 +52,12 @@ properties: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_program_pause + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_remote_start_canceled + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert @@ -55,6 +66,7 @@ properties: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_Rinse_aid_refill_external + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert @@ -67,6 +79,7 @@ properties: device_class: problem icon: mdi:dishwasher-alert - property: Alarm_Sani_program_finished + hide: true binary_sensor: device_class: problem icon: mdi:dishwasher-alert @@ -85,7 +98,9 @@ properties: sensor: writable: true - property: Child_lock + hide: true - property: Child_lock_setting_status + hide: true sensor: writable: false - property: Curent_program_duration @@ -100,16 +115,24 @@ properties: unit: min - property: Current_program_phase - property: Delay_start + hide: true - property: Delay_start_remaining_time + hide: true - property: Delay_start_set_time + hide: true - property: Demo_mode_status + hide: true - property: Device_status - property: Display_Brightness_setting_status + hide: true sensor: unknown_value: 255 - property: Display_contrast_setting_status + hide: true - property: Display_logotype_setting_status + hide: true - property: Door_status + hide: true sensor: device_class: enum options: @@ -120,6 +143,7 @@ properties: sensor: unknown_value: 65535 - property: Energy_save_setting_status + hide: true - property: Error_F70 icon: mdi:dishwasher-alert hide: true @@ -220,8 +244,11 @@ properties: icon: mdi:dishwasher-alert hide: true - property: FOTA_status + hide: true - property: Fan_sequence_setting_status + hide: true - property: Feedback_volumen_setting_status + hide: true - property: Fill_salt binary_sensor: device_class: problem @@ -229,50 +256,92 @@ properties: - property: HardPairingStatus hide: true - property: Heat_pump_setting_status + hide: true - property: High_temperature_status + hide: true - property: Interior_light_at_power_off_setting_status + hide: true - property: Interior_light_status + hide: true - property: Language_status + hide: true - property: Last_run_program_id sensor: unknown_value: 255 - property: MDO_on_demand + hide: true - property: MDO_on_demand_allowed + hide: true - property: Notification_volumen_setting_status + hide: true - property: Odor_control_setting + hide: true - property: Pressure_calibration_setting_status + hide: true - property: Remote_control_monitoring_set_commands + hide: true - property: Remote_control_monitoring_set_commands_actions + hide: true - property: Rinse_aid_refill + hide: true - property: Rinse_aid_setting_status + hide: true - property: Sani_Lock + hide: true - property: Sani_Lock_allowed + hide: true - property: Selected_program_Lower_wash_function + hide: true - property: Selected_program_Upper_wash_function + hide: true - property: Selected_program_auto_door_open_function + hide: true - property: Selected_program_dry_function + hide: true - property: Selected_program_extra_drying_function + hide: true - property: Selected_program_id_status + hide: true - property: Selected_program_mode + hide: true - property: Selected_program_storage_function + hide: true - property: Selected_program_uv_function + hide: true - property: Session_pairing_active + hide: true - property: Session_pairing_confirmation + hide: true - property: Session_pairing_status + hide: true - property: Silence_on_demand + hide: true - property: Silence_on_demand_allowed + hide: true - property: Soft_pairing_status + hide: true - property: Speed_on_demand + hide: true - property: Spend_on_demand_allowed + hide: true - property: Storage_mode_allowed + hide: true - property: Storage_mode_on_demand_stat + hide: true - property: Super_rinse_on_demand + hide: true - property: Super_rinse_on_demand_allowed + hide: true - property: Super_rinse_setting_status + hide: true - property: Super_rinse_status + hide: true - property: Tab_setting_status + hide: true - property: Temperature_unit_status + hide: true - property: Time_program_set_duration_status + hide: true - property: Total_energy_consumption sensor: state_class: total @@ -292,18 +361,26 @@ properties: device_class: water unit: L - property: UV_mode_on_demand + hide: true - property: UV_mode_on_demand_allowed + hide: true - property: Water_consumption_in_running_program + hide: true sensor: unknown_value: 65535 state_class: total device_class: water unit: L - property: Water_hardness_setting_status + hide: true - property: Water_inlet_setting_status + hide: true - property: Water_save_setting_status + hide: true - property: Water_tank + hide: true - property: daily_energy_kwh + hide: true sensor: state_class: total device_class: energy diff --git a/custom_components/connectlife/data_dictionaries/README.md b/custom_components/connectlife/data_dictionaries/README.md index 9ee3979..fabf0f9 100644 --- a/custom_components/connectlife/data_dictionaries/README.md +++ b/custom_components/connectlife/data_dictionaries/README.md @@ -1,21 +1,71 @@ -Data dictionaries for known appliances are located in this directory. +# Data dictionaries -Appliances without data dictionary will be still be loaded. +Data dictionaries for known appliances are located in this directory. Appliances without data dictionary will be still +be loaded, but with a warning in the log. Also, all unknown properties are mapped to hidden status entities. + +To make a property visible by default, just add the property to the list (without setting `hide`). File name: `-.yaml` -List of `properties` with the following items: +The file containse to top level items: +- `device_type`: string +- `properties`: list of [`Property`](#property): + +## Property + +| Item | Type | Description | +|-----------------|------------------------------------|---------------------------------------------------------------------------------------------------| +| `property` | string | Name of status/property. | +| `hide` | `true`, `false` | If Home Assistant should initially hide the sensor entity for this property. Defaults to `false`. | +| `icon` | `mdi:eye`, etc. | Icon to use for the entity. | +| `binary_sensor` | [BinarySensor](#type-binarysensor) | Create a binary sensor of the property. | +| `climate` | [Climate](#type-climate) | Map the property to a climate entity for the device. | +| `select` | [Select](#type-select) | Create a selector of the property. | +| `sensor` | [Sensor](#type-sensor) | Create a sensor of the property. This is the default. | +| `switch` | [Switch](#type-switch) | Create a Switch of the property. | + +If a entity mapping is not given, the property is mapped to a sensor entity. + +It is not necessary to include items with empty values. A [JSON schema](properties-schema.json) is provided so data dictionaries can be +validated. + +## Type `BinarySensor` -| Item | Type | Description | -|-----------------|-----------------|---------------------------------------------------------------------------------------------------| -| `property` | string | Name of status/property. | -| `hide` | `true`, `false` | If Home Assistant should initially hide the sensor entity for this property. Defaults to `false`. | -| `icon` | `mdi:eye`, etc. | Icon to use for the entity. | -| `sensor` | Sensor | Create a sensor of the property. This is the default. | -| `binary_sensor` | BinarySensor | Create a binary sensor of the property. | +Domain `binary_sensor` can be used for read only properties where `0` is not available, `1` is off, and `2` is on. Both +`0`and `1` is mapped to off. + +| Item | Type | Description | +|----------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `device_class` | `power`, `problem`, etc. | For domain `binary_sensor`, name of any [BinarySensorDeviceClass enum](https://developers.home-assistant.io/docs/core/entity/binary-sensor#available-device-classes). | -Type `Sensor` +## Type `Climate`: + +Domain `climate` can be used to map the property to a target propery in a climate entity. If at least one property has +type `climate`, a climate entity is created for the appliance. + +| Item | Type | Description | +|-----------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `target` | string | Any of these [climate entity](https://developers.home-assistant.io/docs/core/entity/climate#properties) attributes: `fan_mode`, `hvac_action`, `swing_mode`, `temperature`, `target_temperature`, `temperature_unit`, or the special target `is_on` | +| `options` | dictionary of integer to string | Required for `fan_mode`, `hvac_action`, `swing_mode`, and `temperature_unit` | + +`temperature_unit` defaults to Celsius. + +Note that `hvac_action` can only be mapped to [pre-defined actions](https://developers.home-assistant.io/docs/core/entity/climate#hvac-action). +If a value does not have a sensible mapping, leave it out to set `hvac_action` to `None` for that value, or consider +mapping to a sensor `enum` instead. + +For `fan_mode` and `swing_mode`, remember to add [transalation string](#translation-strings). + +## Type `Select` + +| Item | Type | Description | +|------------|---------------------------------|-------------| +| `options` | dictionary of integer to string | Required. | + +For remember to add options to [translation strings](#translation-strings). + +## Type `Sensor` | Item | Type | Description | |-----------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -27,37 +77,32 @@ Type `Sensor` | `unit` | `min`, `kWh`, `L`, etc. | Required if `device_class` is set, except not allowed when `device_class` is `ph` or `enum`. | | `options` | dictionary of integer to string | Required if `device_class` is set to `enum`. | -Type `BinarySensor` +For enum options, remember to add [translation strings](#translation-strings). -| Item | Type | Description | -|----------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `device_class` | `power`, `problem`, etc. | For domain `binary_sensor`, name of any [BinarySensorDeviceClass enum](https://developers.home-assistant.io/docs/core/entity/binary-sensor#available-device-classes). | +## Type `Switch` +| Item | Type | Description | +|-------|---------|---------------------------| +| `off` | integer | Off value. Defaults to 0. | +| `on` | integer | On value. Defaults to 1. | -Domain `binary_sensor` can be used for read only properties where `0` is not available, `1` is off, and `2` is on. Both -`0`and `1` is mapped to off. - -If none of `sensor` or `binary_sensor` is provided, the property is treated like `sensor`. It is not necessary to -include items with empty values. A [JSON schema](properties-schema.json) is provided so data dictionaries can be -validated. +# Translation strings By default, sensor entities are named by replacing `_` with ` ` in the property name. However, the property name is also the translation key for the property, so it is possible to add a different English entity name as well as provide translations by adding the property to [strings.json](../strings.json), and then to any [translations](../translations) files. -Options for device class `enum` should always be added. - For example, given the following data dictionary: ```yaml properties: - - property: t_fan_speed + - property: Door_status sensor: device_class: enum options: - 0: low - 1: high - 2: auto + 0: not_available + 1: closed + 2: open ``` This goes into [strings.json](../strings.json) and [en.json](../translations/en.json), @@ -65,12 +110,49 @@ This goes into [strings.json](../strings.json) and [en.json](../translations/e { "entity": { "sensor": { - "t_fan_speed": { - "name": "Fan speed", + "Door_status": { + "name": "Door", "state": { - "low": "Low", - "high": "High", - "auto": "Auto" + "not_available": "Unavailable", + "closed": "Closed", + "open": "Open" + } + } + } + } +} +``` + +Climate modes must be registered as `state_attributes`. + +For example, given the following data dictionary: +```yaml +properties: + - property: t_fan_speed + climate: + target: fan_mode + options: + 0: auto + 5: low + 6: medium_low + 7: medium + 8: medium_high + 9: high +``` + +Strings not in [Home Assistant Core](https://github.com/home-assistant/core/blob/dev/homeassistant/components/climate/strings.json) goes in [strings.json](../strings.json) and [en.json](../translations/en.json): +```json +{ + "entity": { + "climate": { + "connectlife": { + "state_attributes": { + "fan_mode": { + "state": { + "medium_low": "Medium low", + "medium_high": "Medium high" + } + } } } } diff --git a/custom_components/connectlife/data_dictionaries/properties-schema.json b/custom_components/connectlife/data_dictionaries/properties-schema.json index 479dfbe..e8abb1a 100644 --- a/custom_components/connectlife/data_dictionaries/properties-schema.json +++ b/custom_components/connectlife/data_dictionaries/properties-schema.json @@ -6,6 +6,11 @@ "type": "object", "additionalProperties": false, "properties": { + "device_type": { + "title": "Device type", + "type": "string", + "examples": ["dishwasher", "hob", "aircondition"] + }, "properties": { "type": "array", "items": { @@ -27,11 +32,17 @@ "description": "Property name", "type": "string" }, + "binary_sensor": { + "$ref": "#/definitions/BinarySensor" + }, + "select": { + "$ref": "#/definitions/Select" + }, "sensor": { "$ref": "#/definitions/Sensor" }, - "binary_sensor": { - "$ref": "#/definitions/BinarySensor" + "switch": { + "$ref": "#/definitions/Switch" }, "icon": { "type": "string", @@ -41,7 +52,8 @@ }, "hide": { "type": "boolean", - "description": "If Home Assistant should initially hide the sensor entity for this property. Defaults to false." + "description": "If Home Assistant should initially hide the sensor entity for this property.", + "default": false } }, "required": [ @@ -60,6 +72,34 @@ }, "title": "BinarySensor" }, + "Climate" : { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "$ref": "#/definitions/ClimateTarget" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map of integer to string. Required for targets fan_mode, swing_mode, and temeprature_unit." + } + }, + "Select" : { + "type": "object", + "additionalProperties": false, + "properties": { + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Map of integer to string." + }, + "required": ["options"] + }, "Sensor": { "type": "object", "additionalProperties": false, @@ -98,6 +138,26 @@ } } }, + "Switch": { + "type": "object", + "additionalProperties": false, + "properties": { + "off": { + "type": "integer", + "description": "Off value", + "default": "0", + }, + "on": { + "type": "integer", + "description": "On value", + "default": "1", + }, + "device_class": { + "$ref": "#/definitions/SwitchDeviceClass" + } + }, + "title": "BinarySensor" + }, "BinarySensorDeviceClass": { "type": "string", "enum": [ @@ -132,6 +192,50 @@ ], "title": "Device class", "description": "Name of any BinarySensorDeviceClass enum." + }, "BinarySensorDeviceClass": { + "type": "string", + "enum": [ + "battery", + "battery_charging", + "carbon_monoxide", + "cold", + "connectivity", + "door", + "garage_door", + "gas", + "heat", + "light", + "lock", + "moisture", + "motion", + "moving", + "occupancy", + "opening", + "plug", + "power", + "presence", + "problem", + "running", + "safety", + "smoke", + "sound", + "tamper", + "update", + "vibration", + "window" + ], + "title": "Device class", + "description": "Name of any BinarySensorDeviceClass enum." + }, + "CLimateTarget": { + "type": "string", + "enum": [ + "fan_mode", + "swing_mode", + "temperature_unit", + ], + "title": "Climate target.", + "description": "Target property of ClimateEntity." }, "SensorDeviceClass": { "type": "string", @@ -201,5 +305,14 @@ "title": "State class", "description": "Name of any SensorStateClass. Only allowed for integer properties, defaults to measurement." } + }, + "SwitchDeviceClass": { + "type": "string", + "enum": [ + "outlet", + "switch" + ], + "title": "Device class", + "description": "Name of any SwitchDeviceClass enum." } } diff --git a/custom_components/connectlife/dictionaries.py b/custom_components/connectlife/dictionaries.py index b0ef51e..04db431 100644 --- a/custom_components/connectlife/dictionaries.py +++ b/custom_components/connectlife/dictionaries.py @@ -4,12 +4,75 @@ import pkgutil from connectlife.appliance import ConnectLifeAppliance -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import Platform from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.switch import SwitchDeviceClass + +from .const import ( + FAN_MODE, + HVAC_ACTION, + SWING_MODE, + TEMPERATURE_UNIT, +) + +DEVICE_CLASS = "device_class" +HIDE = "hide" +ICON = "icon" +OFF = "off" +ON = "on" +OPTIONS = "options" +PROPERTY = "property" +PROPERTIES = "properties" +MAX_VALUE = "max_value" +TARGET = "target" +STATE_CLASS = "state_class" +SWITCH = "switch" +UNKNOWN_VALUE = "unknown_value" +UNIT = "unit" +WRITABLE = "writable" _LOGGER = logging.getLogger(__name__) +class BinarySensor(): + device_class: BinarySensorDeviceClass | None + + def __init__(self, name: str, binary_sensor: dict | None): + if binary_sensor is None: + binary_sensor = {} + self.device_class = BinarySensorDeviceClass(binary_sensor[DEVICE_CLASS]) \ + if DEVICE_CLASS in binary_sensor else None + + +class Climate(): + target: str + options: dict | None + + def __init__(self, name: str, climate: dict | None): + if climate is None: + climate = {} + self.target = climate[TARGET] if TARGET in climate else None + if self.target is None: + _LOGGER.warning("Missing climate.target for for %s", name) + self.options = climate[OPTIONS] if OPTIONS in climate else None + if self.options is None and self.target in [FAN_MODE, HVAC_ACTION, SWING_MODE, TEMPERATURE_UNIT]: + _LOGGER.warning("Missing climate.options for %s", name) + + +class Select(): + options: dict + + def __init__(self, name: str, select: dict): + if select is None: + select = {} + if not OPTIONS in select: + _LOGGER.warning("Select %s has no options", name) + self.options = [] + else: + self.options = select[OPTIONS] + + class Sensor(): unknown_value: int | None max_value: int | None @@ -19,16 +82,18 @@ class Sensor(): unit: str | None options: list[dict[int, str]] | None - def __init__(self, name: str, sensor: dict = {}): - self.unknown_value = sensor["unknown_value"] if "unknown_value" in sensor and sensor["unknown_value"] else None - self.writable = sensor["writable"] if "writable" in sensor else None - self.max_value = sensor["max_value"] if "max_value" in sensor and sensor["max_value"] else None - self.unit = sensor["unit"] if "unit" in sensor and sensor["unit"] else None - self.state_class = SensorStateClass(sensor["state_class"]) if "state_class" in sensor else None + def __init__(self, name: str, sensor: dict): + if sensor is None: + sensor = {} + self.unknown_value = sensor[UNKNOWN_VALUE] if UNKNOWN_VALUE in sensor and sensor[UNKNOWN_VALUE] else None + self.writable = sensor[WRITABLE] if WRITABLE in sensor else None + self.max_value = sensor[MAX_VALUE] if MAX_VALUE in sensor and sensor[MAX_VALUE] else None + self.unit = sensor[UNIT] if UNIT in sensor and sensor[UNIT] else None + self.state_class = SensorStateClass(sensor[STATE_CLASS]) if STATE_CLASS in sensor else None device_class = None - if "device_class" in sensor: - device_class = SensorDeviceClass(sensor["device_class"]) + if DEVICE_CLASS in sensor: + device_class = SensorDeviceClass(sensor[DEVICE_CLASS]) if device_class == SensorDeviceClass.ENUM: if self.unit: _LOGGER.warning("%s has device class enum, but has unit", name) @@ -51,12 +116,18 @@ def __init__(self, name: str, sensor: dict = {}): self.device_class = device_class -class BinarySensor(): - device_class: BinarySensorDeviceClass | None +class Switch(): + device_class: SwitchDeviceClass | None + off: int + on: int - def __init__(self, name: str, binary_sensor: dict): - self.device_class = BinarySensorDeviceClass(binary_sensor["device_class"])\ - if "device_class" in binary_sensor else None + def __init__(self, name: str, switch: dict): + if switch is None: + switch = {} + self.device_class = SwitchDeviceClass(switch[DEVICE_CLASS])\ + if DEVICE_CLASS in switch else None + self.off = switch[OFF] if OFF in switch else 0 + self.on = switch[ON] if ON in switch else 1 class Property: @@ -64,19 +135,28 @@ class Property: icon: str | None hide: bool binary_sensor: BinarySensor | None + climate: Climate | None sensor: Sensor | None + select: Select | None + switch: Switch | None def __init__(self, entry: dict): - self.name = entry["property"] - self.icon = entry["icon"] if "icon" in entry and entry["icon"] else None - self.hide = entry["hide"] == True if "hide" in entry else False - - if "binary_sensor" in entry: - self.binary_sensor = BinarySensor(self.name, entry["binary_sensor"]) - elif "sensor" in entry: - self.sensor = Sensor(self.name, entry["sensor"]) + self.name = entry[PROPERTY] + self.icon = entry[ICON] if ICON in entry and entry[ICON] else None + self.hide = entry[HIDE] == bool(entry[HIDE]) if HIDE in entry else False + + if Platform.BINARY_SENSOR in entry: + self.binary_sensor = BinarySensor(self.name, entry[Platform.BINARY_SENSOR]) + elif Platform.CLIMATE in entry: + self.climate = Climate(self.name, entry[Platform.CLIMATE]) + elif Platform.SENSOR in entry: + self.sensor = Sensor(self.name, entry[Platform.SENSOR]) + elif Platform.SELECT in entry: + self.select = Select(self.name, entry[Platform.SELECT]) + elif Platform.SWITCH in entry: + self.switch = Switch(self.name, entry[Platform.SWITCH]) else: - self.sensor = Sensor(self.name) + self.sensor = Sensor(self.name, {}) class Dictionaries: @@ -89,12 +169,12 @@ def get_dictionary(cls, appliance: ConnectLifeAppliance) -> dict[str, Property]: key = f"{appliance.device_type_code}-{appliance.device_feature_code}" if key in Dictionaries.dictionaries: return Dictionaries.dictionaries[key] - dictionary = defaultdict(lambda: Property({"property": "default"})) + dictionary = defaultdict(lambda: Property({PROPERTY: "default", HIDE: True})) try: data = pkgutil.get_data(__name__, f"data_dictionaries/{key}.yaml") parsed = yaml.safe_load(data) - for property in parsed["properties"]: - dictionary[property["property"]] = Property(property) + for property in parsed[PROPERTIES]: + dictionary[property[PROPERTY]] = Property(property) except FileNotFoundError: _LOGGER.warning("No data dictionary found for %s", key) Dictionaries.dictionaries[key] = dictionary diff --git a/custom_components/connectlife/entity.py b/custom_components/connectlife/entity.py index f2f96c0..8ccbb48 100644 --- a/custom_components/connectlife/entity.py +++ b/custom_components/connectlife/entity.py @@ -1,12 +1,17 @@ """ConnectLife entity base class.""" +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from connectlife.appliance import ConnectLifeAppliance + from .const import ( DOMAIN, ) from .coordinator import ConnectLifeCoordinator -from connectlife.appliance import ConnectLifeAppliance -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity class ConnectLifeEntity(CoordinatorEntity[ConnectLifeCoordinator]): @@ -23,3 +28,14 @@ def __init__(self, coordinator: ConnectLifeCoordinator, appliance: ConnectLifeAp name=appliance.device_nickname, suggested_area=appliance.room_name, ) + + @callback + @abstractmethod + def update_state(self): + """Subclasses implement this to update their state.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.update_state() + self.async_write_ha_state() diff --git a/custom_components/connectlife/select.py b/custom_components/connectlife/select.py new file mode 100644 index 0000000..0a9c878 --- /dev/null +++ b/custom_components/connectlife/select.py @@ -0,0 +1,80 @@ +"""Provides a selector for ConnectLife.""" +import logging + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DOMAIN, +) +from .coordinator import ConnectLifeCoordinator +from .dictionaries import Dictionaries, Property +from .entity import ConnectLifeEntity +from connectlife.appliance import ConnectLifeAppliance + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ConnectLife selectors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + for appliance in coordinator.appliances.values(): + dictionary = Dictionaries.get_dictionary(appliance) + async_add_entities( + ConnectLifeSelect(coordinator, appliance, s, dictionary[s]) + for s in appliance.status_list if hasattr(dictionary[s], Platform.SELECT) + ) + + +class ConnectLifeSelect(ConnectLifeEntity, SelectEntity): + """Select class for ConnectLife.""" + + _attr_has_entity_name = True + _attr_current_option = None + + def __init__( + self, + coordinator: ConnectLifeCoordinator, + appliance: ConnectLifeAppliance, + status: str, + dd_entry: Property + ): + """Initialize the entity.""" + super().__init__(coordinator, appliance) + self._attr_unique_id = f"{appliance.device_id}-{status}" + self.status = status + self.options_map = dd_entry.select.options + self.reverse_options_map = {v: k for k, v in self.options_map.items()} + self.entity_description = SelectEntityDescription( + key=self._attr_unique_id, + entity_registry_visible_default = not dd_entry.hide, + icon=dd_entry.icon, + name=status.replace("_", " "), + translation_key = status, + options=list(self.options_map.values()) + ) + self.update_state() + + @callback + def update_state(self): + if self.status in self.coordinator.appliances[self.device_id].status_list: + value = self.coordinator.appliances[self.device_id].status_list[self.status] + if value in self.options_map: + value = self.options_map[value] + else: + _LOGGER.warning("Got unexpected value %d for %s", value, self.status) + _value = None + self._attr_current_option = value + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.coordinator.api.update_appliance(self.puid, {self.status: self.reverse_options_map[option]}) + self._attr_current_option = option + self.async_write_ha_state() + diff --git a/custom_components/connectlife/sensor.py b/custom_components/connectlife/sensor.py index af7be45..b6668b9 100644 --- a/custom_components/connectlife/sensor.py +++ b/custom_components/connectlife/sensor.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.components.sensor import SensorEntity, SensorStateClass, SensorDeviceClass, SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,7 +35,7 @@ async def async_setup_entry( dictionary = Dictionaries.get_dictionary(appliance) async_add_entities( ConnectLifeStatusSensor(coordinator, appliance, s, dictionary[s]) - for s in appliance.status_list if hasattr(dictionary[s], "sensor") + for s in appliance.status_list if hasattr(dictionary[s], Platform.SENSOR) ) platform = entity_platform.async_get_current_platform() @@ -94,16 +95,14 @@ def update_state(self): if self.status in self.coordinator.appliances[self.device_id].status_list: value = self.coordinator.appliances[self.device_id].status_list[self.status] if self.device_class == SensorDeviceClass.ENUM: - value = self.options[value] + if value in self.options_map: + value = self.options_map[value] + else: + _LOGGER.warning("Got unexpected value %d for %s", value, self.status) + value = None self._attr_native_value = value if value != self.unknown_value else None self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1 - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self.update_state() - self.async_write_ha_state() - async def async_set_value(self, value: int) -> None: """Set value for this sensor.""" _LOGGER.debug("Setting %s to %d", self.status, value) @@ -117,3 +116,5 @@ async def async_set_value(self, value: int) -> None: await self.coordinator.api.update_appliance(self.puid, {self.status: str(value)}) except LifeConnectError as api_error: raise ServiceValidationError(str(api_error)) from api_error + self._attr_native_value = value + self.async_write_ha_state() diff --git a/custom_components/connectlife/strings.json b/custom_components/connectlife/strings.json index e3980a8..12b4f7a 100644 --- a/custom_components/connectlife/strings.json +++ b/custom_components/connectlife/strings.json @@ -30,6 +30,27 @@ } }, "entity": { + "climate": { + "connectlife": { + "state_attributes": { + "fan_mode": { + "state": { + "middle_low": "Middle low", + "middle_high": "Middle high" + } + }, + "swing_mode": { + "state": { + "both_sides": "Both sides", + "forward": "Forward", + "left": "Left", + "right": "Right", + "swing": "Swing" + } + } + } + } + }, "sensor": { "Alarm_door_closed": { "name": "Door closed" @@ -44,6 +65,38 @@ "closed": "Closed", "open": "Open" } + }, + "t_sleep": { + "name": "Sleep" + } + }, + "switch": { + "t_eco": { + "name": "Eco" + }, + "t_fan_mute": { + "name": "Fan mute" + }, + "t_super": { + "name": "Super" + }, + "t_tms": { + "name": "AI" + } + }, + "select": { + "t_swing_angle": { + "name": "Swing angle", + "state": { + "swing": "Swing", + "auto": "Auto", + "angle_1": "Angle 1", + "angle_2": "Angle 2", + "angle_3": "Angle 3", + "angle_4": "Angle 4", + "angle_5": "Angle 5", + "angle_6": "Angle 6" + } } } } diff --git a/custom_components/connectlife/switch.py b/custom_components/connectlife/switch.py new file mode 100644 index 0000000..ea165b1 --- /dev/null +++ b/custom_components/connectlife/switch.py @@ -0,0 +1,85 @@ +"""Provides a switch for ConnectLife.""" +import logging + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DOMAIN, +) +from .coordinator import ConnectLifeCoordinator +from .dictionaries import Dictionaries, Property +from .entity import ConnectLifeEntity +from connectlife.appliance import ConnectLifeAppliance + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ConnectLife sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + for appliance in coordinator.appliances.values(): + dictionary = Dictionaries.get_dictionary(appliance) + async_add_entities( + ConnectLifeSwitch(coordinator, appliance, s, dictionary[s]) + for s in appliance.status_list if hasattr(dictionary[s], "switch") + ) + + +class ConnectLifeSwitch(ConnectLifeEntity, SwitchEntity): + """Switch class for ConnectLife.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ConnectLifeCoordinator, + appliance: ConnectLifeAppliance, + status: str, + dd_entry: Property + ): + """Initialize the entity.""" + super().__init__(coordinator, appliance) + self._attr_unique_id = f"{appliance.device_id}-{status}" + self.status = status + self.entity_description = SwitchEntityDescription( + key=self._attr_unique_id, + entity_registry_visible_default = not dd_entry.hide, + icon=dd_entry.icon, + name=status.replace("_", " "), + translation_key = status, + device_class = dd_entry.switch.device_class + ) + self.off = dd_entry.switch.off + self.on = dd_entry.switch.on + self.update_state() + + @callback + def update_state(self): + if self.status in self.coordinator.appliances[self.device_id].status_list: + value = self.coordinator.appliances[self.device_id].status_list[self.status] + if value == self.on: + self._attr_is_on = True + elif value == self.off: + self._attr_is_on = False + else: + self._attr_is_on = None + _LOGGER.warning("Unknown value %d for %s", value, self.status) + self._attr_available = self.coordinator.appliances[self.device_id]._offline_state == 1 + + async def async_turn_off(self, **kwargs): + """Turn off.""" + await self.coordinator.api.update_appliance(self.puid, {self.status: str(self.off)}) + self._attr_is_on = False + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs): + """Turn on.""" + await self.coordinator.api.update_appliance(self.puid, {self.status: str(self.on)}) + self._attr_is_on = True + self.async_write_ha_state() diff --git a/custom_components/connectlife/translations/en.json b/custom_components/connectlife/translations/en.json index 62fdf20..84daaad 100644 --- a/custom_components/connectlife/translations/en.json +++ b/custom_components/connectlife/translations/en.json @@ -31,6 +31,27 @@ } }, "entity": { + "climate": { + "connectlife": { + "state_attributes": { + "fan_mode": { + "state": { + "middle_low": "Middle low", + "middle_high": "Middle high" + } + }, + "swing_mode": { + "state": { + "both_sides": "Both sides", + "forward": "Forward", + "left": "Left", + "right": "Right", + "swing": "Swing" + } + } + } + } + }, "sensor": { "Alarm_door_closed": { "name": "Door closed" @@ -45,6 +66,38 @@ "closed": "Closed", "open": "Open" } + }, + "t_sleep": { + "name": "Sleep" + } + }, + "switch": { + "t_eco": { + "name": "Eco" + }, + "t_fan_mute": { + "name": "Fan mute" + }, + "t_super": { + "name": "Super" + }, + "t_tms": { + "name": "AI" + } + }, + "select": { + "t_swing_angle": { + "name": "Swing angle", + "state": { + "swing": "Swing", + "auto": "Auto", + "angle_1": "Angle 1", + "angle_2": "Angle 2", + "angle_3": "Angle 3", + "angle_4": "Angle 4", + "angle_5": "Angle 5", + "angle_6": "Angle 6" + } } } }