From 7680056413c6979245f8f6877340f319f01f972e Mon Sep 17 00:00:00 2001 From: turbozv Date: Fri, 17 Jul 2026 19:09:19 -0700 Subject: [PATCH 1/5] Add battery voltage and current sensors --- CHANGELOG.md | 3 ++ custom_components/pecron/sensor.py | 31 ++++++++++++++------- tests/test_battery_sensors.py | 44 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 tests/test_battery_sensors.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 49079e1..0759aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Battery Voltage and Current sensors**: Exposes battery voltage in volts and battery current in amperes when supported by the device TSL. + ### Changed - Upgraded to unofficial-pecron-api v0.4.1 (adds `eco_onoff_us` as an alternate property code for Eco Silent Mode on some device models) diff --git a/custom_components/pecron/sensor.py b/custom_components/pecron/sensor.py index e7b4716..39d08b7 100644 --- a/custom_components/pecron/sensor.py +++ b/custom_components/pecron/sensor.py @@ -12,7 +12,12 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import UnitOfElectricPotential, UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTime +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTime, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -20,13 +25,7 @@ DataUpdateCoordinator, ) -from .const import ( - ATTR_DEVICE_KEY, - ATTR_FIRMWARE_VERSION, - ATTR_PRODUCT_KEY, - ATTR_PRODUCT_NAME, - DOMAIN, -) +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -62,6 +61,20 @@ def __post_init__(self) -> None: state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="%", ), + PecronSensorDescription( + key="battery_voltage", + name="Battery Voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + ), + PecronSensorDescription( + key="battery_current", + name="Battery Current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + ), PecronSensorDescription( key="total_input_power", name="Input Power", @@ -301,8 +314,6 @@ def native_value(self) -> int | float | None: is_idle = input_power == 0 and output_power == 0 is_charging_only = input_power > 0 and output_power == 0 is_discharging_only = input_power == 0 and output_power > 0 - is_ups_mode = input_power > 0 and output_power > 0 - # Time to Full logic if self.entity_description.key == "remain_charging_time": if is_discharging_only or is_idle: diff --git a/tests/test_battery_sensors.py b/tests/test_battery_sensors.py new file mode 100644 index 0000000..914797f --- /dev/null +++ b/tests/test_battery_sensors.py @@ -0,0 +1,44 @@ +"""Tests for battery electrical measurement sensors.""" + +from unittest.mock import MagicMock + +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential + +from custom_components.pecron.sensor import PECRON_SENSORS, PecronSensor + + +def _sensor_description(key: str): + """Return the description for a sensor key.""" + return next(description for description in PECRON_SENSORS if description.key == key) + + +def _sensor_with_value(key: str, value: float) -> PecronSensor: + """Create a sensor whose coordinator reports a battery measurement.""" + device = MagicMock(device_key="test_device", device_name="Test Device", product_name="Test Product") + properties = MagicMock() + setattr(properties, key, value) + coordinator = MagicMock( + data={"test_device": {"device": device, "properties": properties}} + ) + return PecronSensor(coordinator, "test_device", device, _sensor_description(key)) + + +def test_battery_voltage_sensor_metadata_and_value() -> None: + """Battery voltage is exposed as a voltage measurement in volts.""" + description = _sensor_description("battery_voltage") + + assert description.device_class is SensorDeviceClass.VOLTAGE + assert description.state_class is SensorStateClass.MEASUREMENT + assert description.native_unit_of_measurement == UnitOfElectricPotential.VOLT + assert _sensor_with_value("battery_voltage", 51.2).native_value == 51.2 + + +def test_battery_current_sensor_metadata_and_value() -> None: + """Battery current is exposed as a current measurement in amperes.""" + description = _sensor_description("battery_current") + + assert description.device_class is SensorDeviceClass.CURRENT + assert description.state_class is SensorStateClass.MEASUREMENT + assert description.native_unit_of_measurement == UnitOfElectricCurrent.AMPERE + assert _sensor_with_value("battery_current", -12.5).native_value == -12.5 From 3780c75df74a3419dcdf382712b965d56dcaf584 Mon Sep 17 00:00:00 2001 From: turbozv Date: Fri, 17 Jul 2026 19:56:13 -0700 Subject: [PATCH 2/5] fix: read battery packet measurements (#2) * fix: read battery packet measurements * test: cover battery packet telemetry * feat: add battery temperature sensor --- CHANGELOG.md | 5 +- README.md | 5 +- custom_components/pecron/sensor.py | 47 ++++++++---- tests/test_battery_sensors.py | 114 ++++++++++++++++++++++++++--- 4 files changed, 144 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0759aff..36c7fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **Battery Voltage and Current sensors**: Exposes battery voltage in volts and battery current in amperes when supported by the device TSL. +- **Battery pack sensors**: Exposes battery voltage in volts, signed battery current in amperes, and battery temperature in degrees Celsius when supported by the device TSL. + +### Fixed +- Read battery voltage, current, and temperature from the nested battery packet exposed by the Pecron API and discover all three sensors through its `host_packet_data_jdb` TSL property. ### Changed - Upgraded to unofficial-pecron-api v0.4.1 (adds `eco_onoff_us` as an alternate property code for Eco Silent Mode on some device models) diff --git a/README.md b/README.md index b8a5cd5..0367ca9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Home Assistant community integration for Pecron portable power stations. Monit ## Features - **Device Control** - Turn AC and DC outputs on/off directly from Home Assistant -- **Real-time Monitoring** - Battery percentage, input/output power, and device status +- **Real-time Monitoring** - Battery percentage, voltage, current, temperature, input/output power, and device status - **Multi-device Support** - Manage multiple Pecron stations from one account - **Smart Entity Discovery** - Automatically creates only the entities your device supports - **Advanced Control Service** - `pecron.set_property` for controlling any writable device property @@ -72,6 +72,9 @@ The integration creates the following entities for each device: ### Sensors - **Battery Percentage** - Current battery level (%) +- **Battery Voltage** - Current battery pack voltage (V) +- **Battery Current** - Signed battery pack charge/discharge current (A) +- **Battery Temperature** - Current battery pack temperature (°C) - **Input Power** - Total power being drawn from all input sources (W) - **AC Input Power** - Power from grid/AC charging (W) - **DC Input Power** - Power from solar/DC input (W) diff --git a/custom_components/pecron/sensor.py b/custom_components/pecron/sensor.py index 39d08b7..a38e5de 100644 --- a/custom_components/pecron/sensor.py +++ b/custom_components/pecron/sensor.py @@ -16,6 +16,7 @@ UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfPower, + UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant, callback @@ -34,10 +35,12 @@ class PecronSensorDescription(SensorEntityDescription): """Describe a Pecron sensor.""" + icon: str | None = None always_create: bool = False # Bypass TSL filtering smart_availability: bool = False # Use smart logic for availability struct_property: str | None = None # Parent property name if value is inside a STRUCT dict struct_field: str | None = None # Key within the struct dict to extract + tsl_property: str | None = None # TSL property that supplies this sensor's value def __post_init__(self) -> None: """Post init.""" @@ -67,6 +70,10 @@ def __post_init__(self) -> None: device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + icon="mdi:battery-heart-variant", + struct_property="battery_pack", + struct_field="host_packet_voltage", + tsl_property="host_packet_data_jdb", ), PecronSensorDescription( key="battery_current", @@ -74,6 +81,21 @@ def __post_init__(self) -> None: device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + icon="mdi:current-dc", + struct_property="battery_pack", + struct_field="host_packet_current", + tsl_property="host_packet_data_jdb", + ), + PecronSensorDescription( + key="battery_temperature", + name="Battery Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + icon="mdi:thermometer", + struct_property="battery_pack", + struct_field="host_packet_temp", + tsl_property="host_packet_data_jdb", ), PecronSensorDescription( key="total_input_power", @@ -157,16 +179,16 @@ def create_sensors_for_device(device_key: str, device_data: dict) -> list: for sensor_desc in PECRON_SENSORS: # Always create sensors marked with always_create flag - # Otherwise check both property name and _hm variant (API maps xxx_hm -> xxx) - # For struct sensors, also check the TSL code with _data_ infix - # (e.g., ac_input -> ac_data_input_hm) - tsl_key = sensor_desc.key - tsl_key_hm = f"{sensor_desc.key}_hm" - tsl_key_data_hm = f"{tsl_key.replace('_input', '_data_input')}_hm" if "_input" in tsl_key else None - if (sensor_desc.always_create or - tsl_key in tsl_property_codes or - tsl_key_hm in tsl_property_codes or - (tsl_key_data_hm and tsl_key_data_hm in tsl_property_codes)): + # Otherwise check the API property name and common TSL variants. + tsl_keys = { + sensor_desc.key, + f"{sensor_desc.key}_hm", + sensor_desc.tsl_property or sensor_desc.key, + } + if "_input" in sensor_desc.key: + tsl_keys.add(f"{sensor_desc.key.replace('_input', '_data_input')}_hm") + + if sensor_desc.always_create or tsl_property_codes.intersection(tsl_keys): sensors.append( PecronSensor( coordinator, @@ -177,11 +199,10 @@ def create_sensors_for_device(device_key: str, device_data: dict) -> list: ) else: _LOGGER.debug( - "Skipping sensor '%s' for %s - not in TSL (checked '%s' and '%s_hm')", + "Skipping sensor '%s' for %s - not in TSL (checked %s)", sensor_desc.key, device_data["device"].device_name, - sensor_desc.key, - sensor_desc.key, + sorted(tsl_keys), ) else: # Fallback: create all sensors if TSL is not available diff --git a/tests/test_battery_sensors.py b/tests/test_battery_sensors.py index 914797f..f7ed8f3 100644 --- a/tests/test_battery_sensors.py +++ b/tests/test_battery_sensors.py @@ -1,11 +1,18 @@ """Tests for battery electrical measurement sensors.""" +from types import SimpleNamespace from unittest.mock import MagicMock +import pytest from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfTemperature, +) -from custom_components.pecron.sensor import PECRON_SENSORS, PecronSensor +from custom_components.pecron.const import DOMAIN +from custom_components.pecron.sensor import PECRON_SENSORS, PecronSensor, async_setup_entry def _sensor_description(key: str): @@ -13,15 +20,14 @@ def _sensor_description(key: str): return next(description for description in PECRON_SENSORS if description.key == key) -def _sensor_with_value(key: str, value: float) -> PecronSensor: - """Create a sensor whose coordinator reports a battery measurement.""" - device = MagicMock(device_key="test_device", device_name="Test Device", product_name="Test Product") - properties = MagicMock() - setattr(properties, key, value) - coordinator = MagicMock( - data={"test_device": {"device": device, "properties": properties}} +def _sensor_with_battery_pack(battery_pack: dict | None) -> tuple[MagicMock, MagicMock]: + """Create a battery sensor whose coordinator reports a battery packet.""" + device = MagicMock( + device_key="test_device", device_name="Test Device", product_name="Test Product" ) - return PecronSensor(coordinator, "test_device", device, _sensor_description(key)) + properties = SimpleNamespace(battery_pack=battery_pack) + coordinator = MagicMock(data={"test_device": {"device": device, "properties": properties}}) + return coordinator, device def test_battery_voltage_sensor_metadata_and_value() -> None: @@ -31,7 +37,13 @@ def test_battery_voltage_sensor_metadata_and_value() -> None: assert description.device_class is SensorDeviceClass.VOLTAGE assert description.state_class is SensorStateClass.MEASUREMENT assert description.native_unit_of_measurement == UnitOfElectricPotential.VOLT - assert _sensor_with_value("battery_voltage", 51.2).native_value == 51.2 + assert description.tsl_property == "host_packet_data_jdb" + assert description.struct_property == "battery_pack" + assert description.struct_field == "host_packet_voltage" + + coordinator, device = _sensor_with_battery_pack({"host_packet_voltage": "51.2"}) + sensor = PecronSensor(coordinator, "test_device", device, description) + assert sensor.native_value == 51.2 def test_battery_current_sensor_metadata_and_value() -> None: @@ -41,4 +53,82 @@ def test_battery_current_sensor_metadata_and_value() -> None: assert description.device_class is SensorDeviceClass.CURRENT assert description.state_class is SensorStateClass.MEASUREMENT assert description.native_unit_of_measurement == UnitOfElectricCurrent.AMPERE - assert _sensor_with_value("battery_current", -12.5).native_value == -12.5 + assert description.tsl_property == "host_packet_data_jdb" + assert description.struct_property == "battery_pack" + assert description.struct_field == "host_packet_current" + + coordinator, device = _sensor_with_battery_pack({"host_packet_current": "-12.5"}) + sensor = PecronSensor(coordinator, "test_device", device, description) + assert sensor.native_value == -12.5 + + +def test_battery_temperature_sensor_metadata_and_value() -> None: + """Battery temperature is exposed as a temperature measurement in Celsius.""" + description = _sensor_description("battery_temperature") + + assert description.device_class is SensorDeviceClass.TEMPERATURE + assert description.state_class is SensorStateClass.MEASUREMENT + assert description.native_unit_of_measurement == UnitOfTemperature.CELSIUS + assert description.tsl_property == "host_packet_data_jdb" + assert description.struct_property == "battery_pack" + assert description.struct_field == "host_packet_temp" + + coordinator, device = _sensor_with_battery_pack({"host_packet_temp": "31.5"}) + sensor = PecronSensor(coordinator, "test_device", device, description) + assert sensor.native_value == 31.5 + + +@pytest.mark.parametrize( + ("battery_pack", "expected"), + [ + (None, None), + ({}, None), + ({"host_packet_voltage": None}, None), + ({"host_packet_voltage": "not-a-number"}, None), + ({"host_packet_voltage": "24"}, 24), + ], +) +def test_battery_sensor_handles_missing_and_invalid_values( + battery_pack: dict | None, expected: int | None +) -> None: + """Missing or invalid battery pack telemetry produces an unavailable value.""" + description = _sensor_description("battery_voltage") + coordinator, device = _sensor_with_battery_pack(battery_pack) + sensor = PecronSensor(coordinator, "test_device", device, description) + + assert sensor.native_value == expected + + +@pytest.mark.asyncio +async def test_battery_sensors_created_from_battery_packet_tsl() -> None: + """Battery sensors are created when the device TSL exposes the battery packet.""" + device = MagicMock( + device_key="test_device", + device_name="Test Device", + product_name="Test Product", + ) + coordinator = MagicMock( + data={ + "test_device": { + "device": device, + "properties": SimpleNamespace( + battery_pack={ + "host_packet_voltage": "51.2", + "host_packet_current": "-12.5", + } + ), + "tsl": [SimpleNamespace(code="host_packet_data_jdb")], + } + } + ) + entry = MagicMock(entry_id="test_entry") + hass = MagicMock(data={DOMAIN: {entry.entry_id: coordinator}}) + async_add_entities = MagicMock() + + await async_setup_entry(hass, entry, async_add_entities) + + sensors = async_add_entities.call_args.args[0] + sensor_keys = {sensor.entity_description.key for sensor in sensors} + assert "battery_voltage" in sensor_keys + assert "battery_current" in sensor_keys + assert "battery_temperature" in sensor_keys From 3e7d053864014efe1ce806ddeef2d312b5e26f04 Mon Sep 17 00:00:00 2001 From: turbozv Date: Fri, 17 Jul 2026 20:18:30 -0700 Subject: [PATCH 3/5] feat: add signed battery power sensor --- CHANGELOG.md | 2 +- README.md | 1 + custom_components/pecron/sensor.py | 28 ++++++++++++++ custom_components/pecron/strings.json | 3 ++ tests/test_battery_sensors.py | 53 +++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c7fef..b5e2903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **Battery pack sensors**: Exposes battery voltage in volts, signed battery current in amperes, and battery temperature in degrees Celsius when supported by the device TSL. +- **Battery pack sensors**: Exposes battery voltage in volts, signed battery current in amperes, calculated battery power in watts, and battery temperature in degrees Celsius when supported by the device TSL. Battery power is positive while charging and negative while discharging. ### Fixed - Read battery voltage, current, and temperature from the nested battery packet exposed by the Pecron API and discover all three sensors through its `host_packet_data_jdb` TSL property. diff --git a/README.md b/README.md index 0367ca9..a7cf2f4 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ The integration creates the following entities for each device: - **Battery Percentage** - Current battery level (%) - **Battery Voltage** - Current battery pack voltage (V) - **Battery Current** - Signed battery pack charge/discharge current (A) +- **Battery Power** - Current battery power (W), positive while charging and negative while discharging - **Battery Temperature** - Current battery pack temperature (°C) - **Input Power** - Total power being drawn from all input sources (W) - **AC Input Power** - Power from grid/AC charging (W) diff --git a/custom_components/pecron/sensor.py b/custom_components/pecron/sensor.py index a38e5de..6fe9ca8 100644 --- a/custom_components/pecron/sensor.py +++ b/custom_components/pecron/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import math from dataclasses import dataclass from typing import Any @@ -86,6 +87,16 @@ def __post_init__(self) -> None: struct_field="host_packet_current", tsl_property="host_packet_data_jdb", ), + PecronSensorDescription( + key="battery_power", + name="Battery Power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + icon="mdi:battery-charging", + struct_property="battery_pack", + tsl_property="host_packet_data_jdb", + ), PecronSensorDescription( key="battery_temperature", name="Battery Temperature", @@ -298,6 +309,23 @@ def native_value(self) -> int | float | None: props = self.coordinator.data[self._device_key]["properties"] + # Battery power is derived from the signed current and voltage in the + # battery packet. The current sign makes charging positive and + # discharging negative. + if self.entity_description.key == "battery_power": + battery_pack = getattr(props, "battery_pack", None) + if not battery_pack or not isinstance(battery_pack, dict): + return None + + try: + voltage = float(battery_pack["host_packet_voltage"]) + current = float(battery_pack["host_packet_current"]) + except (KeyError, TypeError, ValueError): + return None + + power = voltage * current + return power if math.isfinite(power) else None + # For struct sensors, extract the value from the parent dict if self.entity_description.struct_property and self.entity_description.struct_field: struct_dict = getattr(props, self.entity_description.struct_property, None) diff --git a/custom_components/pecron/strings.json b/custom_components/pecron/strings.json index 21ab6bf..b93941e 100644 --- a/custom_components/pecron/strings.json +++ b/custom_components/pecron/strings.json @@ -32,6 +32,9 @@ "battery_percentage": { "name": "Battery percentage" }, + "battery_power": { + "name": "Battery power" + }, "total_input_power": { "name": "Input power" }, diff --git a/tests/test_battery_sensors.py b/tests/test_battery_sensors.py index f7ed8f3..1edb4b1 100644 --- a/tests/test_battery_sensors.py +++ b/tests/test_battery_sensors.py @@ -8,6 +8,7 @@ from homeassistant.const import ( UnitOfElectricCurrent, UnitOfElectricPotential, + UnitOfPower, UnitOfTemperature, ) @@ -62,6 +63,57 @@ def test_battery_current_sensor_metadata_and_value() -> None: assert sensor.native_value == -12.5 +@pytest.mark.parametrize( + ("current", "expected"), + [ + ("10", 512.0), + ("-12.5", -640.0), + ("0", 0.0), + ], +) +def test_battery_power_sensor_metadata_and_signed_value(current: str, expected: float) -> None: + """Battery power is voltage times signed current in watts.""" + description = _sensor_description("battery_power") + + assert description.device_class is SensorDeviceClass.POWER + assert description.state_class is SensorStateClass.MEASUREMENT + assert description.native_unit_of_measurement == UnitOfPower.WATT + assert description.tsl_property == "host_packet_data_jdb" + assert description.struct_property == "battery_pack" + + coordinator, device = _sensor_with_battery_pack( + {"host_packet_voltage": "51.2", "host_packet_current": current} + ) + sensor = PecronSensor(coordinator, "test_device", device, description) + + assert sensor.native_value == expected + + +@pytest.mark.parametrize( + "battery_pack", + [ + None, + {}, + {"host_packet_voltage": "51.2"}, + {"host_packet_current": "10"}, + {"host_packet_voltage": None, "host_packet_current": "10"}, + {"host_packet_voltage": "invalid", "host_packet_current": "10"}, + {"host_packet_voltage": "51.2", "host_packet_current": "invalid"}, + {"host_packet_voltage": "nan", "host_packet_current": "10"}, + {"host_packet_voltage": "51.2", "host_packet_current": "inf"}, + ], +) +def test_battery_power_sensor_handles_missing_and_invalid_values( + battery_pack: dict | None, +) -> None: + """Battery power is unavailable unless voltage and current are numeric.""" + description = _sensor_description("battery_power") + coordinator, device = _sensor_with_battery_pack(battery_pack) + sensor = PecronSensor(coordinator, "test_device", device, description) + + assert sensor.native_value is None + + def test_battery_temperature_sensor_metadata_and_value() -> None: """Battery temperature is exposed as a temperature measurement in Celsius.""" description = _sensor_description("battery_temperature") @@ -131,4 +183,5 @@ async def test_battery_sensors_created_from_battery_packet_tsl() -> None: sensor_keys = {sensor.entity_description.key for sensor in sensors} assert "battery_voltage" in sensor_keys assert "battery_current" in sensor_keys + assert "battery_power" in sensor_keys assert "battery_temperature" in sensor_keys From 7751117b068bb8fbdf3009bddbfab2d8b944cfe0 Mon Sep 17 00:00:00 2001 From: turbozv Date: Fri, 17 Jul 2026 20:20:33 -0700 Subject: [PATCH 4/5] fix: set battery sensor display precision --- CHANGELOG.md | 3 +++ custom_components/pecron/sensor.py | 2 ++ tests/test_battery_sensors.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e2903..6468889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Battery pack sensors**: Exposes battery voltage in volts, signed battery current in amperes, calculated battery power in watts, and battery temperature in degrees Celsius when supported by the device TSL. Battery power is positive while charging and negative while discharging. +### Changed +- Battery voltage defaults to two decimal places and battery power defaults to one decimal place in Home Assistant displays. + ### Fixed - Read battery voltage, current, and temperature from the nested battery packet exposed by the Pecron API and discover all three sensors through its `host_packet_data_jdb` TSL property. diff --git a/custom_components/pecron/sensor.py b/custom_components/pecron/sensor.py index 6fe9ca8..9cc9b8c 100644 --- a/custom_components/pecron/sensor.py +++ b/custom_components/pecron/sensor.py @@ -71,6 +71,7 @@ def __post_init__(self) -> None: device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=2, icon="mdi:battery-heart-variant", struct_property="battery_pack", struct_field="host_packet_voltage", @@ -93,6 +94,7 @@ def __post_init__(self) -> None: device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=1, icon="mdi:battery-charging", struct_property="battery_pack", tsl_property="host_packet_data_jdb", diff --git a/tests/test_battery_sensors.py b/tests/test_battery_sensors.py index 1edb4b1..4125ee3 100644 --- a/tests/test_battery_sensors.py +++ b/tests/test_battery_sensors.py @@ -38,6 +38,7 @@ def test_battery_voltage_sensor_metadata_and_value() -> None: assert description.device_class is SensorDeviceClass.VOLTAGE assert description.state_class is SensorStateClass.MEASUREMENT assert description.native_unit_of_measurement == UnitOfElectricPotential.VOLT + assert description.suggested_display_precision == 2 assert description.tsl_property == "host_packet_data_jdb" assert description.struct_property == "battery_pack" assert description.struct_field == "host_packet_voltage" @@ -78,6 +79,7 @@ def test_battery_power_sensor_metadata_and_signed_value(current: str, expected: assert description.device_class is SensorDeviceClass.POWER assert description.state_class is SensorStateClass.MEASUREMENT assert description.native_unit_of_measurement == UnitOfPower.WATT + assert description.suggested_display_precision == 1 assert description.tsl_property == "host_packet_data_jdb" assert description.struct_property == "battery_pack" From 3a3732b6e1ef8bdb99a6ba9006126aa91453a528 Mon Sep 17 00:00:00 2001 From: jsightler Date: Thu, 30 Jul 2026 19:36:53 -0400 Subject: [PATCH 5/5] Add strings.json translation entries for new battery sensors battery_power got an entry but battery_voltage, battery_current, and battery_temperature did not, so they were falling back to the description name instead of the translation-driven path used by every other sensor. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 +----- custom_components/pecron/strings.json | 9 +++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6468889..65a37e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Battery voltage defaults to two decimal places and battery power defaults to one decimal place in Home Assistant displays. - -### Fixed -- Read battery voltage, current, and temperature from the nested battery packet exposed by the Pecron API and discover all three sensors through its `host_packet_data_jdb` TSL property. - -### Changed - Upgraded to unofficial-pecron-api v0.4.1 (adds `eco_onoff_us` as an alternate property code for Eco Silent Mode on some device models) ### Fixed +- Read battery voltage, current, and temperature from the nested battery packet exposed by the Pecron API and discover all three sensors through its `host_packet_data_jdb` TSL property. - **Crash on setup when no devices are usable or initial fetch fails**: replaced all uses of the removed `hass.components.persistent_notification` accessor with the current `homeassistant.components.persistent_notification.async_create(hass, ...)` API. Previously, any code path that tried to show a persistent notification (no devices found, initial connection failure, invalid/read-only property, failed switch/select control) raised `AttributeError: 'HomeAssistant' object has no attribute 'components'` on modern Home Assistant, aborting integration setup entirely instead of surfacing the intended message (#8) ## [0.5.0] - 2026-04-10 diff --git a/custom_components/pecron/strings.json b/custom_components/pecron/strings.json index b93941e..576242f 100644 --- a/custom_components/pecron/strings.json +++ b/custom_components/pecron/strings.json @@ -32,9 +32,18 @@ "battery_percentage": { "name": "Battery percentage" }, + "battery_voltage": { + "name": "Battery voltage" + }, + "battery_current": { + "name": "Battery current" + }, "battery_power": { "name": "Battery power" }, + "battery_temperature": { + "name": "Battery temperature" + }, "total_input_power": { "name": "Input power" },