Skip to content

Commit

Permalink
Merge
Browse files Browse the repository at this point in the history
  • Loading branch information
CJNE committed May 7, 2023
2 parents 0e2c87d + d524f07 commit 09f279d
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/constraints.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pip==23.0.1
pre-commit==3.2.0
black==23.1.0
pre-commit==3.2.1
black==23.3.0
flake8==6.0.0
reorder-python-imports==3.9.0
12 changes: 10 additions & 2 deletions custom_components/sunspec/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sunspec2.modbus.client import SunSpecModbusClientTimeout
from sunspec2.modbus.modbus import ModbusClientError

TIMEOUT = 60
TIMEOUT = 120


_LOGGER: logging.Logger = logging.getLogger(__package__)
Expand Down Expand Up @@ -69,6 +69,12 @@ def getPoint(self, point_name, model_index=0):
)


# pragma: not covered
def progress(msg):
_LOGGER.debug(msg)
return True


class SunSpecApiClient:
CLIENT_CACHE = {}

Expand Down Expand Up @@ -150,7 +156,9 @@ def modbus_connect(self, config=None):
f"Failed to connect to {self._host}:{self._port} slave id {self._slave_id}"
)
_LOGGER.debug("Client connected, perform initial scan")
client.scan(connect=False)
client.scan(
connect=False, progress=progress, full_model_read=False, delay=0.5
)
return client
except ModbusClientError:
raise ConnectionError(
Expand Down
7 changes: 6 additions & 1 deletion custom_components/sunspec/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"dependencies": [],
"config_flow": true,
"codeowners": ["@cjne"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/cjne/ha-sunspec",
"iot_class": "local_polling",
"requirements": ["pysunspec2==1.0.8"]
"issue_tracker": "https://github.com/cjne/ha-sunspec/issues",
"requirements": ["pysunspec2==1.0.8"],
"version": "0.0.22"
}
73 changes: 57 additions & 16 deletions custom_components/sunspec/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from homeassistant.components.sensor import DEVICE_CLASS_ENERGY
from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE
from homeassistant.components.sensor import DEVICE_CLASS_VOLTAGE
from homeassistant.components.sensor import (
SensorDeviceClass,
)
from homeassistant.components.sensor import RestoreSensor
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING
Expand Down Expand Up @@ -56,6 +55,7 @@
"Mbps": [DATA_RATE_MEGABITS_PER_SECOND, ICON_DEFAULT, None],
"V": [ELECTRIC_POTENTIAL_VOLT, ICON_VOLT, DEVICE_CLASS_VOLTAGE],
"VA": [POWER_VOLT_AMPERE, ICON_POWER, None],
"VAr": [POWER_VOLT_AMPERE, ICON_POWER, None],
"W": [POWER_WATT, ICON_POWER, DEVICE_CLASS_POWER],
"W/m2": [IRRADIATION_WATTS_PER_SQUARE_METER, ICON_DEFAULT, None],
"Wh": [ENERGY_WATT_HOUR, ICON_ENERGY, DEVICE_CLASS_ENERGY],
Expand All @@ -71,6 +71,8 @@
"mm": [LENGTH_MILLIMETERS, ICON_DEFAULT, None],
"%": [PERCENTAGE, ICON_DEFAULT, None],
"Secs": [TIME_SECONDS, ICON_DEFAULT, None],
"enum16": [None, ICON_DEFAULT, SensorDeviceClass.ENUM],
"bitfield32": [None, ICON_DEFAULT, SensorDeviceClass.ENUM],
}


Expand All @@ -92,7 +94,20 @@ async def async_setup_entry(hass, entry, async_add_devices):
"model": model_wrapper,
"prefix": prefix,
}
sensors.append(SunSpecSensor(coordinator, entry, data))

meta = model_wrapper.getMeta(key)
sunspec_unit = meta.get("units", "")
ha_meta = HA_META.get(sunspec_unit, [sunspec_unit, None, None])
device_class = ha_meta[2]
if sunspec_unit == "":
_LOGGER.debug("No unit for")
_LOGGER.debug(meta)
if device_class == DEVICE_CLASS_ENERGY:
_LOGGER.debug("Adding energy sensor")
sensors.append(SunSpecEnergySensor(coordinator, entry, data))
else:
sensors.append(SunSpecSensor(coordinator, entry, data))

async_add_devices(sensors)


Expand All @@ -110,7 +125,7 @@ def __init__(self, coordinator, config_entry, data):
self._meta = self.model_wrapper.getMeta(self.key)
self._group_meta = self.model_wrapper.getGroupMeta()
self._point_meta = self.model_wrapper.getPoint(self.key).pdef
sunspec_unit = self._meta.get("units", "")
sunspec_unit = self._meta.get("units", self._meta.get("type", ""))
ha_meta = HA_META.get(sunspec_unit, [sunspec_unit, ICON_DEFAULT, None])
self.unit = ha_meta[0]
self.use_icon = ha_meta[1]
Expand Down Expand Up @@ -197,16 +212,6 @@ def native_value(self):
"Math overflow error when retreiving calculated value for %s", self.key
)
return None
# If this is an energy sensor a value of 0 woulld mess up long term stats because of how total_increasing works
if self.use_device_class == DEVICE_CLASS_ENERGY:
if val == 0:
_LOGGER.debug(
"Returning last known value instead of 0 for {self.name) to avoid resetting total_increasing counter"
)
self._assumed_state = True
return self.lastKnown
self.lastKnown = val
self._assumed_state = False
vtype = self._meta["type"]
if vtype in ("enum16", "bitfield32"):
symbols = self._meta.get("symbols", None)
Expand All @@ -228,6 +233,9 @@ def native_value(self):
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
# if self.unit == "":
# _LOGGER.debug(f"UNIT IS NONT FOR {self.name}")
# return None
return self.unit

@property
Expand All @@ -243,7 +251,7 @@ def device_class(self):
@property
def state_class(self):
"""Return de device class of the sensor."""
if self.unit == "":
if self.unit == "" or self.unit is None:
return None
if self.device_class == DEVICE_CLASS_ENERGY:
return STATE_CLASS_TOTAL_INCREASING
Expand All @@ -266,3 +274,36 @@ def extra_state_attributes(self):
self.key, self.model_index
)
return attrs


class SunSpecEnergySensor(SunSpecSensor, RestoreSensor):
def __init__(self, coordinator, config_entry, data):
super().__init__(coordinator, config_entry, data)
self.last_known_value = None

@property
def native_value(self):
val = super().native_value
# For an energy sensor a value of 0 woulld mess up long term stats because of how total_increasing works
if val == 0:
_LOGGER.debug(
"Returning last known value instead of 0 for {self.name) to avoid resetting total_increasing counter"
)
self._assumed_state = True
return self.lastKnown
self.lastKnown = val
self._assumed_state = False
return val

async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
await super().async_added_to_hass()
_LOGGER.debug(f"{self.name} Fetch last known state")
state = await self.async_get_last_sensor_data()
if state:
_LOGGER.debug(
f"{self.name} Got last known value from state: {state.native_value}"
)
self.last_known_value = state.native_value
else:
_LOGGER.debug(f"{self.name} No previous state was found")
6 changes: 4 additions & 2 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pre-commit==3.2.0
black==23.1.0
flake8==6.0.0
reorder-python-imports==3.9.0
homeassistant
pysunspec2==1.0.8
flake8
reorder-python-imports
1 change: 1 addition & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
-r requirements_dev.txt
pre-commit==3.2.0
pytest-homeassistant-custom-component
serial
pytest-mock
5 changes: 3 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ not_skip = __init__.py
force_sort_within_sections = true
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
default_section = THIRDPARTY
known_first_party = custom_components.sunspec, tests
known_first_party = custom_components.myenergi, tests
combine_as_imports = true

[tool:pytest]
addopts = -qq --cov=custom_components.sunspec
console_output_style = count
asyncio_mode = auto

[coverage:run]
branch = False

[coverage:report]
show_missing = true
fail_under = 100
fail_under = 95
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
TEST_SERIAL_NO = "abc123"
TEST_INVERTER_SENSOR_STATE_ENTITY_ID = "sensor.inverter_operating_state"
TEST_INVERTER_SENSOR_POWER_ENTITY_ID = "sensor.inverter_watts"
TEST_INVERTER_SENSOR_VAR_ID = "sensor.inverter_var"
TEST_INVERTER_SENSOR_ENERGY_ENTITY_ID = "sensor.inverter_watthours"
TEST_INVERTER_MM_SENSOR_STATE_ENTITY_ID = "sensor.dermeasureac_1_operating_state"
TEST_INVERTER_MM_SENSOR_POWER_ENTITY_ID = "sensor.dermeasureac_1_active_power"
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class MockFileClientDevice(modbus_client.FileClientDevice):
def is_connected(self):
return True

def scan(self, progress=None):
print(progress)
if progress is not None:
if not progress("Mock scan"):
return
return super().scan()

def connect(self):
return True

Expand Down
15 changes: 13 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,32 @@ async def test_api(hass, sunspec_client_mock):
assert len(keys) == 22


async def test_get_client(hass, sunspec_modbus_client_mock):
SunSpecApiClient.CLIENT_CACHE = {}
"""Test API calls."""

# To test the api submodule, we first create an instance of our API client
api = SunSpecApiClient(host="test", port=123, slave_id=1, hass=hass)
client = api.get_client()
client.scan.assert_called_once()

SunSpecApiClient.CLIENT_CACHE = {}


async def test_modbus_connect(hass, sunspec_modbus_client_mock):
SunSpecApiClient.CLIENT_CACHE = {}
"""Test API calls."""

# To test the api submodule, we first create an instance of our API client
api = SunSpecApiClient(host="test", port=123, slave_id=1, hass=hass)
SunSpecApiClient.CLIENT_CACHE = {}
client = api.get_client()
client.scan.assert_called_once()

SunSpecApiClient.CLIENT_CACHE = {}


async def test_modbus_connect_fail(hass, mocker):

mocker.patch(
# api_call is from slow.py but imported to main.py
"sunspec2.modbus.client.SunSpecModbusClientDeviceTCP.connect",
Expand All @@ -82,7 +94,6 @@ async def test_modbus_connect_fail(hass, mocker):


async def test_modbus_connect_exception(hass, mocker):

mocker.patch(
# api_call is from slow.py but imported to main.py
"sunspec2.modbus.client.SunSpecModbusClientDeviceTCP.connect",
Expand Down
5 changes: 4 additions & 1 deletion tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
@pytest.fixture(autouse=True)
def bypass_setup_fixture():
"""Prevent setup."""
with patch("custom_components.sunspec.async_setup", return_value=True,), patch(
with patch(
"custom_components.sunspec.async_setup",
return_value=True,
), patch(
"custom_components.sunspec.async_setup_entry",
return_value=True,
):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import TEST_INVERTER_SENSOR_ENERGY_ENTITY_ID
from . import TEST_INVERTER_SENSOR_POWER_ENTITY_ID
from . import TEST_INVERTER_SENSOR_STATE_ENTITY_ID
from . import TEST_INVERTER_SENSOR_VAR_ID
from .const import MOCK_CONFIG_MM
from .const import MOCK_CONFIG_PREFIX

Expand All @@ -35,6 +36,15 @@ async def test_sensor_dc(hass: HomeAssistant, sunspec_client_mock) -> None:
assert entity_state.attributes["icon"] == ICON_DC_AMPS


async def test_sensor_var(hass: HomeAssistant, sunspec_client_mock) -> None:
"""Verify device information includes expected details."""

await setup_mock_sunspec_config_entry(hass)

entity_state = hass.states.get(TEST_INVERTER_SENSOR_VAR_ID)
assert entity_state


async def test_sensor_with_prefix(hass: HomeAssistant, sunspec_client_mock) -> None:
"""Verify device information includes expected details."""

Expand Down

0 comments on commit 09f279d

Please sign in to comment.