Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1e8a05f
First commit
dontinelli Mar 30, 2026
3ed97d1
Add VSCode testing environment
dontinelli Apr 3, 2026
78c9d35
Rename function for clarity
dontinelli Apr 3, 2026
1c31338
minor adjustments
dontinelli Apr 4, 2026
e72679f
Add token to body in API request
dontinelli Mar 30, 2026
bfffcbf
update coverage
dontinelli Mar 30, 2026
4ef02f1
add comment
dontinelli Mar 30, 2026
05e528b
Bump version from 0.7.0 to 0.7.1
dontinelli Mar 31, 2026
8779ed3
First commit
dontinelli Mar 30, 2026
355b485
Tidy up
dontinelli May 13, 2026
dc512db
Fix lint-errors
dontinelli May 13, 2026
8c5bafc
test fixes
dontinelli May 13, 2026
6cf3d2d
linter
dontinelli May 13, 2026
86dbd81
update coverage
dontinelli May 13, 2026
9c7c6ce
split line
dontinelli May 13, 2026
a4904e5
delete whitespace
dontinelli May 13, 2026
7462e97
First commit
dontinelli Mar 30, 2026
60abf7c
update coverage
dontinelli May 13, 2026
6cd42d2
update data type
dontinelli May 14, 2026
e957486
Add .coverage to .gitignore
dontinelli May 14, 2026
99abe85
Remove .coverage
dontinelli May 14, 2026
7ce2cdd
Add token to body in API request
dontinelli Mar 30, 2026
3495266
update coverage
dontinelli Mar 30, 2026
f9fd2e4
Reduce noice in log
dontinelli May 14, 2026
e5da90c
Merge branch 'main' into add_new_datapoints
dontinelli Jun 29, 2026
7a4936c
Fix typing error
dontinelli Jul 6, 2026
131a309
migrate to aiointercept
dontinelli Jul 6, 2026
17f3be2
Minor adjustments to test
dontinelli Aug 1, 2026
ad99d1b
Adjustments to env
dontinelli Aug 1, 2026
da465a1
Add test fixture for solarlog_connector
dontinelli Aug 1, 2026
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
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"image": "mcr.microsoft.com/devcontainers/base:bullseye",

"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"ghcr.io/devcontainers/features/docker-in-docker:4.0.0": {
"moby": true,
"azureDnsAutoDetection": true,
"installDockerBuildx": true,
Expand All @@ -24,7 +24,7 @@
"ghcr.io/devcontainers-community/npm-features/prettier:1": {
"version": "latest"
},
"ghcr.io/hspaans/devcontainer-features/pytest:1": {
"ghcr.io/hspaans/devcontainer-features/pytest:2.0.0": {
"version": "latest",
"plugins": "pytest-testinfra"
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13"]
python-version: ["3.14"]
steps:
- name: ⤵️ Checkout the repository
uses: actions/checkout@v4.1.7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.13'
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: 🏗 Set up Python
uses: actions/setup-python@v5.0.0
with:
python-version: "3.13"
python-version: "3.14"
cache: "pip"
- name: 🏗 Install requirements
run: python3 -m pip install -r requirements.txt
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ on:
workflow_dispatch:

env:
DEFAULT_PYTHON: "3.13"
DEFAULT_PYTHON: "3.14"

jobs:
pytest:
name: Python ${{ matrix.python }}
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.13"]
python: ["3.14"]
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v4.1.7
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/typing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
- main

env:
DEFAULT_PYTHON: "3.13"
DEFAULT_PYTHON: "3.14"

jobs:
mypy:
Expand Down
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@
"python.analysis.extraPaths": [
"./src/solarlog_cli"
],
"python.analysis.typeCheckingMode": "basic"
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": true,
"python.testing.pytestEnabled": true,
"python.createEnvironment.contentButton": "show",
"python-envs.defaultEnvManager": "ms-python.python:system"
}
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.3
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
32 changes: 29 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 All @@ -23,6 +23,15 @@ class SolarLogConnector:
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments

async def __aenter__(self) -> "SolarLogConnector":
"""Enter async context manager."""
return self

async def __aexit__(self, exc_type, exc_value, traceback) -> None:
"""Exit async context manager."""
if not self.client.session.closed:
await self.client.close()

def __init__(
self,
host: str,
Expand Down Expand Up @@ -164,13 +173,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 +203,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
Loading
Loading