Skip to content

Commit

Permalink
Support for inverters with JSON output (#15)
Browse files Browse the repository at this point in the history
* Add options to read JSON input data

* Add tests

* Add pytest to project

* Fix linting errors

* Rename parameter for data request

* Add param to read the JSON
  • Loading branch information
klaasnicolaas authored Aug 31, 2021
1 parent 8a7cb03 commit 0e3473e
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 80 deletions.
101 changes: 101 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: Testing

# yamllint disable-line rule:truthy
on: [push, pull_request]

jobs:
pytest:
name: Python ${{ matrix.python }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ubuntu, windows, macos]
python: [3.8, 3.9]
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v2.3.4
- name: 🏗 Set up Python ${{ matrix.python }}
id: python
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.python }}
- name: 🏗 Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: ⤵️ Restore cached Python PIP packages
uses: actions/cache@v2.1.6
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('.github/workflows/requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-
- name: 🏗 Install workflow dependencies
run: |
pip install -r .github/workflows/requirements.txt
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
- name: ⤵️ Restore cached Python virtual environment
id: cached-poetry-dependencies
uses: actions/cache@v2.1.6
with:
path: .venv
key: venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
restore-keys: |
venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-
- name: 🏗 Install dependencies
run: poetry install --no-interaction
- name: 🚀 Run pytest
run: poetry run pytest --cov omnikinverter tests
- name: ⬆️ Upload coverage artifact
uses: actions/upload-artifact@v2.2.4
with:
name: coverage-${{ matrix.python }}-${{ matrix.os }}
path: .coverage

coverage:
runs-on: ubuntu-latest
needs: pytest
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v2.3.4
- name: ⬇️ Download coverage data
uses: actions/download-artifact@v2.0.10
- name: 🏗 Set up Python 3.9
id: python
uses: actions/setup-python@v2.2.2
with:
python-version: 3.9
- name: 🏗 Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: ⤵️ Restore cached Python PIP packages
uses: actions/cache@v2.1.6
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('.github/workflows/requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-
- name: 🏗 Install workflow dependencies
run: |
pip install -r .github/workflows/requirements.txt
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
- name: ⤵️ Restore cached Python virtual environment
id: cached-poetry-dependencies
uses: actions/cache@v2.1.6
with:
path: .venv
key: >-
venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-
- name: 🏗 Install dependencies
run: poetry install --no-interaction
- name: 🚀 Process coverage results
run: |
poetry run coverage combine coverage*/.coverage*
poetry run coverage xml -i
- name: 🚀 Upload coverage report
uses: codecov/codecov-action@v2.0.3
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ repos:
language: system
types: [python]
entry: poetry run pylint
- id: pytest
name: 🧪 Running tests and test coverage with pytest
language: system
types: [python]
entry: poetry run pytest
pass_filenames: false
- id: pyupgrade
name: 🆙 Checking for upgradable syntax with pyupgrade
language: system
Expand Down
30 changes: 27 additions & 3 deletions omnikinverter/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Models for Omnik Inverter."""
from __future__ import annotations

import json
import re
from dataclasses import dataclass
from typing import Any
Expand All @@ -17,12 +18,33 @@ class Inverter:
solar_energy_today: float | None
solar_energy_total: float | None

@staticmethod
def from_json(data: dict[str, Any]) -> Inverter:
"""Return Inverter object from the Omnik Inverter response.
Args:
data: The JSON data from the Omnik Inverter.
Returns:
An Inverter object.
"""

data = json.loads(data)
return Inverter(
serial_number=data["g_sn"],
model=data["i_modle"],
firmware=data["i_ver_m"],
solar_current_power=data["i_pow_n"],
solar_energy_today=float(data["i_eday"]),
solar_energy_total=float(data["i_eall"]),
)

@staticmethod
def from_js(data: dict[str, Any]) -> Inverter:
"""Return Inverter object from the Omnik Inverter response.
Args:
data: The data from the Omnik Inverter.
data: The JS (webscraping) data from the Omnik Inverter.
Returns:
An Inverter object.
Expand All @@ -35,8 +57,10 @@ def get_values(position):
matches = re.search(r'(?<=myDeviceArray\[0\]=").*?(?=";)', data)

data_list = matches.group(0).split(",")
if position in [6, 7]:
return int(data_list[position]) / 100
if position in [5, 6, 7]:
if position == 5:
return int(data_list[position])
return float(data_list[position]) / 100
return data_list[position]

return Inverter(
Expand Down
16 changes: 13 additions & 3 deletions omnikinverter/omnikinverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@ class OmnikInverter:
"""Main class for handling connections with the Omnik Inverter."""

def __init__(
self, host: str, request_timeout: int = 10, session: ClientSession | None = None
self,
host: str,
use_json: bool,
request_timeout: int = 10,
session: ClientSession | None = None,
) -> None:
"""Initialize connection with the Omnik Inverter.
Args:
host: Hostname or IP address of the Omnik Inverter.
use_json: Boolean to confirm you use a JSON input.
request_timeout: An integer with the request timeout in seconds.
session: Optional, shared, aiohttp client session.
"""
self._session = session
self._close_session = False

self.host = host
self.use_json = use_json
self.request_timeout = request_timeout

async def request(
Expand All @@ -58,7 +64,7 @@ async def request(
url = URL.build(scheme="http", host=self.host, path="/").join(URL(uri))

headers = {
"Accept": "text/html, application/xhtml+xml, application/xml",
"Accept": "text/html,application/xhtml+xml,application/xml",
}

if self._session is None:
Expand All @@ -83,8 +89,9 @@ async def request(
"Error occurred while communicating with Omnik Inverter device"
) from exception

types = ["application/json", "application/x-javascript"]
content_type = response.headers.get("Content-Type", "")
if "application/x-javascript" not in content_type:
if not any(item in content_type for item in types):
text = await response.text()
raise OmnikInverterError(
"Unexpected response from the Omnik Inverter device",
Expand All @@ -99,6 +106,9 @@ async def inverter(self) -> Inverter:
Returns:
A Inverter data object from the Omnik Inverter.
"""
if self.use_json:
data = await self.request("status.json", params={"CMD": "inv_query"})
return Inverter.from_json(data)
data = await self.request("js/status.js")
return Inverter.from_js(data)

Expand Down
Loading

0 comments on commit 0e3473e

Please sign in to comment.