Skip to content
Draft
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
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
"python.analysis.extraPaths": [
"./src/solarlog_cli"
],
"python.analysis.typeCheckingMode": "basic"
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.createEnvironment.contentButton": "show"
}
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"label": "Pytest",
"type": "shell",
"command": "pytest tests",
"command": "pytest tests --snapshot-update",
"dependsOn": ["Install all Requirements"],
"group": {
"kind": "test",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
aiohttp>=3.13.3
aioresponses == 0.7.6
aiohttp>=3.14.1
aiointercept >= 0.1.8
bcrypt >= 4.3.0
mashumaro>=3.13
pytest-aio==1.9.0
Expand Down
90 changes: 71 additions & 19 deletions src/solarlog_cli/solarlog_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import asyncio
from datetime import datetime
from datetime import datetime, date
import json
import logging
from typing import Any
Expand All @@ -17,7 +17,7 @@
SolarLogUpdateError,
)

from .solarlog_models import EnergyData, SolarlogData
from .solarlog_models import EnergyData, EventData, SolarlogData

SOLARLOG_REQUEST_PAYLOAD = '{ "801": { "170": null } }'
_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -72,7 +72,7 @@ async def login(self) -> bool:
response = await self.execute_http_request(payload, "login")

text = await response.text()
_LOGGER.debug("Response: %s", text)
_LOGGER.debug("Response to request for login: %s", text)
if text.count("FAILED - User was wrong"):
# Response means, that no password is required
self.password = ""
Expand Down Expand Up @@ -125,7 +125,7 @@ async def login(self) -> bool:

return True

async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long
async def execute_http_request(self, body: str, path: str = "getjp", timeout: float | None = None) -> ClientResponse: # pylint: disable=line-too-long
"""Helper function to process the HTTP Get call."""
if self.session is None:
self.session = ClientSession()
Expand All @@ -138,8 +138,8 @@ async def execute_http_request(self, body: str, path: str = "getjp", timeout: fl
if self._token != "":
body = f"token={self._token}; " + body

_LOGGER.debug("HTTP-request header: %s", header)
_LOGGER.debug("HTTP-request body: %s", body)
# _LOGGER.debug("HTTP-request header: %s", header)
# _LOGGER.debug("HTTP-request body: %s", body)

try:
response = await self.session.post(
Expand All @@ -163,14 +163,14 @@ async def execute_http_request(self, body: str, path: str = "getjp", timeout: fl
{"Content-Type": content_type, "response": text},
)

_LOGGER.debug("HTTP-request successful: %s", response)
# _LOGGER.debug("HTTP-request successful: %s", response)
return response

async def parse_http_response(self, response: ClientResponse) -> dict[str, Any]:
"""Helper function to parse the HTTP response."""

text = await response.text(errors="replace")
_LOGGER.debug("Parsing http response: %s", text)
# _LOGGER.debug("Parsing http response: %s", text)

if text.count('{"QUERY IMPOSSIBLE 000"}'):
raise SolarLogUpdateError(f"Server response: {text}")
Expand Down Expand Up @@ -221,7 +221,7 @@ async def get_basic_data(self) -> SolarlogData:
return data

async def get_battery_data(self, timeout: float | None = None) -> list[float]:
"""Get battery data from Solar-Log"""
"""Get battery data from Solar-Log."""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "858": null }', timeout=timeout)
Expand All @@ -232,7 +232,7 @@ async def get_battery_data(self, timeout: float | None = None) -> list[float]:
return data

async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
"""Get power data from Solar-Log"""
"""Get power data from Solar-Log."""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "782": null }', timeout=timeout)
Expand All @@ -244,7 +244,7 @@ async def get_power_per_inverter(self, timeout: float | None = None) -> dict[int
return data

async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[int, float]:
"""Get power data from Solar-Log"""
"""Get power data from Solar-Log."""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "854": null }', timeout=timeout)
Expand All @@ -260,7 +260,7 @@ async def get_energy_per_inverter(self, timeout: float | None = None) -> dict[in
return data

async def get_energy(self, timeout: float | None = None) -> EnergyData | None:
"""Get energy data from Solar-Log"""
"""Get energy data from Solar-Log."""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "878": null }', timeout=timeout)
Expand All @@ -274,29 +274,81 @@ async def get_energy(self, timeout: float | None = None) -> EnergyData | None:

return None

async def get_device_list(self, timeout: float | None = None) -> dict[int, str]:
"""Get list of all connected devices."""
async def get_firmware(self, timeout: float | None = None) -> tuple[str, date]:
"""Get firmware data from Solar-Log."""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request(
'{ "801": {"101" : None, "102" : None } }', timeout=timeout
)
)
fw_version: str = raw_data["801"]["101"]
fw_date: date = datetime.strptime(
raw_data["801"]["102"], "%d.%m.%Y").date()

return (fw_version, fw_date)

async def get_device_list(
self,
timeout: float | None = None
) -> dict[int, tuple[str, dict, dict]]:
"""Get list of all connected devices.
Return value is a dict with name, possible events and error codes per device."""

# get list of all inverters connected to Solar-Log
raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "740": null }', timeout=timeout)
)
raw_data = raw_data["740"]

device_list: dict[int, str] = {}
device_list: dict[int, tuple[str, dict, dict]] = {}

for key, value in raw_data.items():
if value != "Err":
# get name of the inverter
query = (
f'{{ "141": {{ "{key}": {{ "119": null, "708": null, '
f'"709": null }} }} }}'
)
raw_data = await self.parse_http_response(
await self.execute_http_request(
f"""{{ "141": {{ "{key}": {{ "119": null }} }} }}""", timeout=timeout
)
await self.execute_http_request(query, timeout=timeout)
)
device_list |= {int(key): raw_data["141"][key]["119"]}
device_list |= {int(key): (
raw_data["141"][key]["119"],
raw_data["141"][key]["708"],
raw_data["141"][key]["709"],
)
}

return device_list

async def get_device_last_event(self, device: int, timeout: float | None = None) -> EventData:
"""Get list of last event/error of a device."""

raw_data = await self.parse_http_response(
await self.execute_http_request(
f"""{{ "141": {{ "{device}": {{ "710": null }} }} }}""", timeout=timeout
)
)
print(raw_data)
events = raw_data["141"][str(device)]["710"]["0"]

return EventData(
start=datetime.fromtimestamp(events[0][0]),
end=datetime.fromtimestamp(events[0][1]),
event=int(events[0][3]),
error=int(events[0][4])
)

async def get_status_per_device(self, timeout: float | None = None) -> dict[int, str]:
"""Get inverter status from Solar-Log"""

raw_data: dict = await self.parse_http_response(
await self.execute_http_request('{ "608": null }', timeout=timeout)
)

return raw_data["608"]

async def close(self) -> None:
"""Close open client session."""
if self.session and self._close_session:
Expand Down
23 changes: 20 additions & 3 deletions src/solarlog_cli/solarlog_connector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Connector class to manage access to Solar-Log."""

from datetime import timezone, tzinfo
from datetime import date, timezone, tzinfo
import logging
from zoneinfo import ZoneInfo

Expand Down Expand Up @@ -164,13 +164,22 @@ async def update_device_list(self, timeout: float | None = None) -> dict[int, In
devices = await self.client.get_device_list(timeout)

self._device_list = {
key: InverterData(name=value,enabled=self.device(key).enabled)
key: InverterData(
name=value[0],
enabled=self.device(key).enabled,errors=value[2],
events=value[1]
)
for key, value in devices.items()
}
_LOGGER.debug("Device list: %s",self._device_list)
_LOGGER.debug("Device list updated: %s",self._device_list)

return self._device_list

async def update_firmware_information(self, timeout: float | None = None) -> tuple[str, date]:
"""Update firmware data (version and relase date)."""

return await self.client.get_firmware(timeout)

async def update_inverter_data(self, timeout: float | None = None) -> dict[int, InverterData]:
"""Update device specific data."""

Expand All @@ -185,6 +194,14 @@ async def update_inverter_data(self, timeout: float | None = None) -> dict[int,
if self._device_list.get(key,InverterData).enabled:
self._device_list[key].consumption_year = float(value)

raw_status_data = await self.client.get_status_per_device(timeout)
for key, value in raw_status_data.items():
key = int(key)
if self._device_list.get(key,InverterData).enabled:
self._device_list[key].status = value

self._device_list[key].last_event = await self.client.get_device_last_event(key)

_LOGGER.debug("Inverter data updated: %s",self._device_list)

return self._device_list
Expand Down
23 changes: 20 additions & 3 deletions src/solarlog_cli/solarlog_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Models for SolarLog."""
from dataclasses import dataclass, field
from datetime import datetime
from datetime import date, datetime

from mashumaro import DataClassDictMixin

Expand All @@ -20,14 +20,29 @@ class EnergyData():
production: float | None = None
self_consumption: float | None = None

@dataclass
class EventData():
"""Event Data model."""

end: datetime
error: int
event: int
start: datetime

@dataclass
class InverterData():
"""Inverter Data model."""

# pylint: disable=too-many-instance-attributes

name: str = ""
enabled: bool = False
current_power: float | None = None
consumption_year: float | None = None
current_power: float | None = None
enabled: bool = False
errors: dict[int,str] = field(default_factory=dict)
events: dict[int,str] = field(default_factory=dict)
last_event: EventData | None = None
status: str | None = None


@dataclass
Expand Down Expand Up @@ -63,6 +78,8 @@ class SolarlogData(DataClassDictMixin):

#extended data
battery_data: BatteryData | None = None
firmware_date: date | None = None
firmware_version: str | None = None
inverter_data: dict[int, InverterData] = field(default_factory=dict)
production_year: float | None = None
self_consumption_year: float | None = None
Loading
Loading