diff --git a/README.md b/README.md index 300ed74..58f015c 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ As soon as HomeAssistant is restarted, you can proceed with __component setup__. ## Supported device models -| Model | Version | -|----------------------------------|--------------------| -| `Refoss Smart Wi-Fi Switch, R11` | `all` | -| `Refoss Smart Wi-Fi Plug, P11S` | `all` | -| `Refoss Smart Wi-Fi Switch, R21` | `all` | +| Model | Version | +|-------------------------------------|--------------------| +| `Refoss Smart Wi-Fi Switch, R11` | `all` | +| `Refoss Smart Wi-Fi Plug, P11S` | `all` | +| `Refoss Smart Wi-Fi Switch, R21` | `all` | +| `Refoss Smart Energy Monito, EM06P` | `all` | diff --git a/custom_components/refoss_rpc/coordinator.py b/custom_components/refoss_rpc/coordinator.py index fcd9107..226167f 100644 --- a/custom_components/refoss_rpc/coordinator.py +++ b/custom_components/refoss_rpc/coordinator.py @@ -238,7 +238,8 @@ def _async_device_event_handler(self, event_data: dict[str, Any]) -> None: if event_type is None: continue - if event_type in ("config_changed"): + RELOAD_EVENTS = {"config_changed", "emmerge_change", "cfg_change"} + if event_type in RELOAD_EVENTS: LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, diff --git a/custom_components/refoss_rpc/cover.py b/custom_components/refoss_rpc/cover.py index 1c188d6..d54397e 100644 --- a/custom_components/refoss_rpc/cover.py +++ b/custom_components/refoss_rpc/cover.py @@ -44,15 +44,14 @@ def __init__(self, coordinator: RefossCoordinator, _id: int) -> None: """Initialize cover.""" super().__init__(coordinator, f"cover:{_id}") self._id = _id - if self.status["pos_control"]: + if self.status["cali_state"] == "success": self._attr_supported_features |= CoverEntityFeature.SET_POSITION @property def current_cover_position(self) -> int | None: """Position of the cover.""" - if not self.status["pos_control"]: + if not self.status["cali_state"] or self.status["cali_state"] != "success": return None - return cast(int, self.status["current_pos"]) @property diff --git a/custom_components/refoss_rpc/entity.py b/custom_components/refoss_rpc/entity.py index a28bc79..c1e2941 100644 --- a/custom_components/refoss_rpc/entity.py +++ b/custom_components/refoss_rpc/entity.py @@ -22,6 +22,7 @@ async_remove_refoss_entity, get_refoss_entity_name, get_refoss_key_instances, + merge_channel_get_status, ) @@ -35,38 +36,51 @@ def async_setup_entry_refoss( ) -> None: """Set up entities for Refoss.""" coordinator = config_entry.runtime_data.coordinator - assert coordinator - if not coordinator.device.initialized: + # If the device is not initialized, return directly + if not coordinator or not coordinator.device.initialized: return - entities = [] - for sensor_id in sensors: - description = sensors[sensor_id] - key_instances = get_refoss_key_instances( - coordinator.device.status, description.key - ) + device_status = coordinator.device.status + device_config = coordinator.device.config + mac = coordinator.mac + entities: list[Any] = [] + + for sensor_id, description in sensors.items(): + key_instances = get_refoss_key_instances(device_status, description.key) for key in key_instances: - # Filter non-existing sensors - if description.sub_key not in coordinator.device.status[ - key - ] and not description.supported(coordinator.device.status[key]): + key_status = device_status.get(key) + if key_status is None: + continue + + # Filter out sensors that are not supported or do not match the configuration + if ( + not key.startswith("emmerge:") + and description.sub_key not in key_status + and not description.supported(key_status) + ): continue - # Filter and remove entities that according to config/status - # should not create an entity + # Filter and remove entities that should not be created according to the configuration/status if description.removal_condition and description.removal_condition( - coordinator.device.config, coordinator.device.status, key + device_config, device_status, key ): - domain = sensor_class.__module__.split(".")[-1] - unique_id = f"{coordinator.mac}-{key}-{sensor_id}" + try: + domain = sensor_class.__module__.split(".")[-1] + except AttributeError: + LOGGER.error( + "Failed to get module name from sensor_class for sensor_id %s and key %s", + sensor_id, + key, + ) + continue + unique_id = f"{mac}-{key}-{sensor_id}" async_remove_refoss_entity(hass, domain, unique_id) else: entities.append(sensor_class(coordinator, key, sensor_id, description)) - if not entities: - return - async_add_entities(entities) + if entities: + async_add_entities(entities) @dataclass(frozen=True, kw_only=True) @@ -101,11 +115,13 @@ def available(self) -> bool: return super().available and (coordinator.device.initialized) @property - def status(self) -> dict: + def status(self) -> dict | None: """Device status by entity key.""" - return cast(dict, self.coordinator.device.status[self.key]) + device_status = self.coordinator.device.status.get(self.key) + if device_status is None: + LOGGER.debug("Device status not found for key: %s", self.key) + return device_status - # pylint: disable-next=hass-missing-super-call async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.coordinator.async_add_listener(self._update_callback)) @@ -164,18 +180,51 @@ def __init__( self._last_value = None @property - def sub_status(self) -> Any: - """Device status by entity key.""" - return self.status[self.entity_description.sub_key] + def sub_status(self) -> Any | None: + """Get the sub - status of the device by entity key. + + Returns the value corresponding to the sub - key in the device status. + If the device status is None or the sub - key does not exist, returns None. + """ + device_status = self.status + if device_status is None: + LOGGER.debug("Device status is None for entity %s", self.name) + return None + sub_key = self.entity_description.sub_key + sub_status = device_status.get(sub_key) + return sub_status @property def attribute_value(self) -> StateType: """Value of sensor.""" - if self.entity_description.value is not None: - self._last_value = self.entity_description.value( - self.status.get(self.entity_description.sub_key), self._last_value + try: + if self.key.startswith("emmerge:"): + # Call the merge channel attributes function + return merge_channel_get_status( + self.coordinator.device.status, + self.key, + self.entity_description.sub_key, + ) + + # Reduce repeated calls and get the sub-status + sub_status = self.sub_status + + if self.entity_description.value is not None: + # Call the custom value processing function + self._last_value = self.entity_description.value( + sub_status, self._last_value + ) + else: + self._last_value = sub_status + + return self._last_value + except Exception as e: + # Log the exception + LOGGER.error( + "Error getting attribute value for entity %s, key %s, attribute %s: %s", + self.name, + self.key, + self.attribute, + str(e), ) - else: - self._last_value = self.sub_status - - return self._last_value + return None diff --git a/custom_components/refoss_rpc/manifest.json b/custom_components/refoss_rpc/manifest.json index e117d92..a9c17e9 100644 --- a/custom_components/refoss_rpc/manifest.json +++ b/custom_components/refoss_rpc/manifest.json @@ -10,7 +10,7 @@ "loggers": ["aiorefoss"], "quality_scale": "bronze", "requirements": ["aiorefoss==1.0.1"], - "version": "1.0.3", + "version": "1.0.5", "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/custom_components/refoss_rpc/sensor.py b/custom_components/refoss_rpc/sensor.py index 0696c9c..ce701cb 100644 --- a/custom_components/refoss_rpc/sensor.py +++ b/custom_components/refoss_rpc/sensor.py @@ -122,6 +122,177 @@ class RefossSensorDescription(RefossEntityDescription, SensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "em_power": RefossSensorDescription( + key="em", + sub_key="power", + name="Power", + native_unit_of_measurement=UnitOfPower.WATT, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "em_voltage": RefossSensorDescription( + key="em", + sub_key="voltage", + name="Voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "em_current": RefossSensorDescription( + key="em", + sub_key="current", + name="Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + "em_month_energy": RefossSensorDescription( + key="em", + sub_key="month_energy", + name="This Month Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_month_ret_energy": RefossSensorDescription( + key="em", + sub_key="month_ret_energy", + name="This Month Return Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_week_energy": RefossSensorDescription( + key="em", + sub_key="week_energy", + name="This Week Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_week_ret_energy": RefossSensorDescription( + key="em", + sub_key="week_ret_energy", + name="This Week Retrun Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_day_energy": RefossSensorDescription( + key="em", + sub_key="day_energy", + name="Today Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_day_ret_energy": RefossSensorDescription( + key="em", + sub_key="day_ret_energy", + name="Today Return Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "em_pf": RefossSensorDescription( + key="em", + sub_key="pf", + name="Power factor", + value=lambda status, _: None if status is None else float(status), + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + "emmerge_power": RefossSensorDescription( + key="emmerge", + sub_key="power", + name="Power", + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "emmerge_current": RefossSensorDescription( + key="emmerge", + sub_key="current", + name="Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + suggested_display_precision=2, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + "emmerge_month_energy": RefossSensorDescription( + key="emmerge", + sub_key="month_energy", + name="This Month Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "emmerge_month_ret_energy": RefossSensorDescription( + key="emmerge", + sub_key="month_ret_energy", + name="This Month Return Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "emmerge_week_energy": RefossSensorDescription( + key="emmerge", + sub_key="week_energy", + name="This Week Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "emmerge_week_ret_energy": RefossSensorDescription( + key="emmerge", + sub_key="week_ret_energy", + name="This Week Retrun Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "emmerge_day_energy": RefossSensorDescription( + key="emmerge", + sub_key="day_energy", + name="Today Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + "emmerge_day_ret_energy": RefossSensorDescription( + key="emmerge", + sub_key="day_ret_energy", + name="Today Return Energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), } diff --git a/custom_components/refoss_rpc/utils.py b/custom_components/refoss_rpc/utils.py index 0cbac00..50c7e30 100644 --- a/custom_components/refoss_rpc/utils.py +++ b/custom_components/refoss_rpc/utils.py @@ -52,8 +52,12 @@ def get_refoss_channel_name(device: RpcDevice, key: str) -> str: if entity_name is None: channel = key.split(":")[0] channel_id = key.split(":")[-1] - if key.startswith(("input:", "switch:","cover:")): + if key.startswith(("input:", "switch:", "cover:", "em:")): return f"{device_name} {channel.title()} {channel_id}" + + if key.startswith(("emmerge:")): + return f"{device_name} {channel.title()}" + return device_name return entity_name @@ -145,6 +149,39 @@ def is_refoss_wifi_stations_disabled( return False +def merge_channel_get_status(_status: dict[str, Any], key: str, attr: str) -> Any: + """ + Merge channel attributes. If the key starts with 'emmerge:', sum the attribute values of the corresponding bits. + + :param _status: Device status dictionary + :param key: Key name + :param attr: Attribute name + :return: Merged attribute value or None + """ + if not key.startswith("emmerge:"): + return None + + try: + # Extract and convert the number + num = int(key.split(":")[1]) + except (IndexError, ValueError): + LOGGER.error("Failed to extract or convert number from key: %s", key) + return None + + # Find the indices of bits with a value of 1 in the binary representation + bit_positions = [i for i in range(num.bit_length()) if num & (1 << i)] + + val = 0 + for bit in bit_positions: + status_key = f"em:{bit+1}" + if status_key in _status and attr in _status[status_key]: + val += _status[status_key][attr] + else: + LOGGER.warning("Missing key %s or attribute %s in status", status_key, attr) + + return val + + def get_host(host: str) -> str: """Get the device IP address.""" try: