Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic sensors #109

Merged
merged 22 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@

import pysma

# This example will work with Python 3.7+
# Python 3.4+ "@asyncio.coroutine" decorator
# Python 3.5+ uses "async def f()" syntax
# Python 3.7+ provides asyncio.run(). For earlier versions the loop should be created manually.
# This example will work with Python 3.9+

_LOGGER = logging.getLogger(__name__)

Expand Down
161 changes: 67 additions & 94 deletions pysma/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
from . import definitions
from .const import (
DEFAULT_TIMEOUT,
DEVCLASS_INVERTER,
DEVICE_INFO,
ENERGY_METER_VIA_INVERTER,
FALLBACK_DEVICE_INFO,
GENERIC_SENSORS,
OPTIMIZERS_VIA_INVERTER,
URL_ALL_PARAMS,
URL_ALL_VALUES,
URL_DASH_LOGGER,
URL_DASH_VALUES,
URL_LOGGER,
Expand Down Expand Up @@ -299,16 +301,13 @@ async def read(self, sensors: Sensors) -> bool:
"keys": list({s.key for s in sensors if s.enabled}),
}
result_body = await self._read_body(URL_VALUES, payload)
if not result_body:
return False

notfound = []
devclass = await self.get_devclass(result_body)
l10n = await self._read_l10n()
for sen in sensors:
if sen.enabled:
if sen.key in result_body:
sen.extract_value(result_body, l10n, str(devclass))
sen.extract_value(result_body, l10n)
continue

notfound.append(f"{sen.name} [{sen.key}]")
Expand Down Expand Up @@ -386,101 +385,75 @@ async def device_info(self) -> dict:

return device_info

async def get_devclass(self, result_body: Optional[dict] = None) -> Optional[str]:
"""Get the device class.

Args:
result_body (dict, optional): result body to extract device class from.
Defaults to None.

Raises:
KeyError: More than 1 device class key is not supported

Returns:
str: The device class identifier, or None if no identifier was found
"""
if self._devclass:
return self._devclass

if not result_body or not isinstance(result_body, dict):
# Read the STATUS_SENSOR.
# self.read will call get_devclass and update self.devclass
await self.read(Sensors(definitions.status))
else:
sensor_values = list(result_body.values())
devclass_keys = list(sensor_values[0].keys())
if len(devclass_keys) == 0:
return None
if devclass_keys[0] == "val":
self._devclass = DEVCLASS_INVERTER
elif len(devclass_keys) > 1:
raise KeyError("More than 1 device class key is not supported")
else:
self._devclass = devclass_keys[0]
_LOGGER.debug("Found device class %s", self._devclass)

return self._devclass
async def _read_all_sensors(self) -> dict:
all_values = await self._read_body(URL_ALL_VALUES, {"destDev": []})
all_params = await self._read_body(URL_ALL_PARAMS, {"destDev": []})
return all_values | all_params

async def get_sensors(self) -> Sensors:
"""Get the sensors based on the device class.
"""Get the sensors that are present on the device.

Returns:
Sensors: Sensors object containing Sensor objects
"""
# Fallback to DEVCLASS_INVERTER if devclass returns None
devclass = await self.get_devclass() or DEVCLASS_INVERTER

_LOGGER.debug("Loading sensors for device class %s", devclass)
device_sensors = Sensors(definitions.sensor_map.get(devclass))

if devclass == DEVCLASS_INVERTER:
em_sensor = copy.copy(definitions.energy_meter)
payload = {
"destDev": [],
"keys": [
em_sensor.key,
definitions.optimizer_serial.key,
],
}
result_body = await self._read_body(URL_VALUES, payload)
all_sensors = await self._read_all_sensors()
sensor_keys = all_sensors.keys()
device_sensors = Sensors()

_LOGGER.debug("Matching generic sensors")

for sensor in definitions.sensor_map[GENERIC_SENSORS]:
if sensor.key in sensor_keys:
sensors_values = list(all_sensors[sensor.key].values())[0]
val_len = len(sensors_values)
_LOGGER.debug("Found %s with %d value(s).", sensor.key, val_len)

if sensor.key_idx < val_len:
_LOGGER.debug(
"Adding sensor %s (%s_%s)",
sensor.name,
sensor.key,
sensor.key_idx,
)
device_sensors.add(sensor)

_LOGGER.debug("Checking if Energy Meter is present...")
# Detect and add Energy Meter sensors
em_sensor = copy.copy(definitions.energy_meter)
em_sensor.extract_value(all_sensors)

if em_sensor.value:
_LOGGER.debug(
"Energy Meter with serial %s detected. Adding extra sensors.",
em_sensor.value,
)
device_sensors.add(
[
sensor
for sensor in definitions.sensor_map[ENERGY_METER_VIA_INVERTER]
if sensor not in device_sensors
]
)

if not result_body:
return device_sensors

# Detect and add Energy Meter sensors
em_sensor.extract_value(result_body)

if em_sensor.value:
_LOGGER.debug(
"Energy Meter with serial %s detected. Adding extra sensors.",
em_sensor.value,
)
device_sensors.add(
[
sensor
for sensor in definitions.sensor_map[ENERGY_METER_VIA_INVERTER]
if sensor not in device_sensors
]
)

# Detect and add Optimizer Sensors
optimizers = result_body.get(definitions.optimizer_serial.key)
if optimizers:
serials = optimizers.get(DEVCLASS_INVERTER)

for idx, serial in enumerate(serials or []):
if serial["val"]:
_LOGGER.debug(
"Optimizer %s with serial %s detected. Adding extra sensors.",
idx,
serial,
)
for sensor_definition in definitions.sensor_map[
OPTIMIZERS_VIA_INVERTER
]:
new_sensor = copy.copy(sensor_definition)
new_sensor.key_idx = idx
new_sensor.name = f"{sensor_definition.name}_{idx}"
device_sensors.add(new_sensor)
_LOGGER.debug("Finding connected optimizers...")
# Detect and add Optimizer Sensors
optimizers = all_sensors.get(definitions.optimizer_serial.key)
if optimizers:
serials = optimizers.popitem()[1]

for idx, serial in enumerate(serials or []):
if serial["val"]:
_LOGGER.debug(
"Optimizer %s with serial %s detected. Adding extra sensors.",
idx,
serial,
)
for sensor_definition in definitions.sensor_map[
OPTIMIZERS_VIA_INVERTER
]:
new_sensor = copy.copy(sensor_definition)
new_sensor.key_idx = idx
new_sensor.name = f"{sensor_definition.name}_{idx}"
device_sensors.add(new_sensor)

return device_sensors
14 changes: 7 additions & 7 deletions pysma/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
URL_LOGIN = "/dyn/login.json"
URL_LOGOUT = "/dyn/logout.json"
URL_VALUES = "/dyn/getValues.json"
URL_ALL_VALUES = "/dyn/getAllOnlValues.json"
URL_ALL_PARAMS = "/dyn/getAllParamValues.json"
URL_LOGGER = "/dyn/getLogger.json"
URL_DASH_LOGGER = "/dyn/getDashLogger.json"
URL_DASH_VALUES = "/dyn/getDashValues.json"
Expand All @@ -13,7 +15,7 @@
JMESPATH_VAL = "val"
JMESPATH_VAL_TAG = JMESPATH_VAL + "[0].tag"
JMESPATH_VAL_STR = "[?str==sum([`1`,`{}`])].val | [0]"
JMESPATH_VAL_IDX = '"{}"[{}].val'
JMESPATH_VAL_IDX = "* | [0][{}].val"
JMESPATH_VAL_IDX_TAG = JMESPATH_VAL_IDX + "[0].tag"

JMESPATHS_TAG = (JMESPATH_VAL_IDX_TAG, JMESPATH_VAL_TAG)
Expand All @@ -37,9 +39,7 @@
"serial": "9999999999",
}

DEVCLASS_INVERTER = "1"
DEVCLASS_BATTERY = "7"
DEVCLASS_ENERGY_METER = "65"
OPTIMIZERS_VIA_INVERTER = "253"
ENERGY_METER_VIA_INVERTER = "254"
DEVICE_INFO = "255"
GENERIC_SENSORS = "generic"
OPTIMIZERS_VIA_INVERTER = "optimizers"
ENERGY_METER_VIA_INVERTER = "energy-meter"
DEVICE_INFO = "device-info"
Loading