Skip to content
Open
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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.git
.github
__pycache__/
*.py[cod]
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.ruff_cache/
solismon3/
examples/
64 changes: 64 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Publish Docker image

on:
push:
branches:
- master
workflow_dispatch:

permissions:
contents: read
packages: write

concurrency:
group: docker-image-${{ github.ref }}
cancel-in-progress: true

jobs:
publish:
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v6.0.2

- name: Set image reference
run: echo "IMAGE_REF=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV"

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_REF }}
tags: |
type=raw,value=latest

- name: Build and push image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_VERSION=latest
BUILD_ARCH=aarch64|amd64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
22 changes: 16 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
FROM python:3-alpine

ARG BUILD_VERSION=latest
ARG BUILD_ARCH="aarch64|amd64"

EXPOSE 18000

LABEL MAINTAINER="Andrius Kozeniauskas"
LABEL NAME=solismon3
LABEL maintainer="dnviti" \
org.opencontainers.image.source="https://github.com/dnviti/solismon3" \
org.opencontainers.image.description="Solis inverter monitor for MQTT, Prometheus, and Home Assistant" \
io.hass.version="${BUILD_VERSION}" \
io.hass.type="app" \
io.hass.arch="${BUILD_ARCH}"

RUN mkdir /solismon3
COPY *.py *.txt /solismon3/
ENV PYTHONUNBUFFERED=1

WORKDIR /solismon3

COPY requirements.txt ./
RUN pip install --upgrade pip \
&& pip3 install -r requirements.txt
&& pip3 install --no-cache-dir -r requirements.txt

COPY main.py ./
COPY config ./config

CMD [ "python", "./main.py" ]
CMD [ "python", "./main.py" ]
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Run main.py
Docker images is provided.
On your docker host create a folder solismon3/config and copy your modified config.py and registers.py files in there
```
docker run -it -d --restart unless-stopped --name solismon3 -v /solismon3/config:/solismon3/config -p 18000:18000 nosireland/solismon3
docker run -it -d --restart unless-stopped --name solismon3 -v /solismon3/config:/solismon3/config -p 18000:18000 ghcr.io/dnviti/solismon3:latest
```

### Docker Compose example
Expand All @@ -69,7 +69,7 @@ version: "3.4"

services:
solismon3:
image: nosireland/solismon3:latest
image: ghcr.io/dnviti/solismon3:latest
container_name: solismon3
restart: always
ports:
Expand All @@ -81,6 +81,14 @@ services:
max-size: 5m
```

### Home Assistant app
This repository can be added to Home Assistant as an app repository:
```
https://github.com/dnviti/solismon3
```
Comment on lines +84 to +88

The app uses the prebuilt `ghcr.io/dnviti/solismon3:latest` image. The GitHub workflow only publishes that `latest` image from the `master` branch.

### Testing
To see if it is running properly I would advise enabling debugging `DEBUG = False` and also `PROMETHEUS = True`
(you do not need to have Prometheus installed) for testing. Once started check container logs `docker logs solismon3` and in
Expand Down
75 changes: 61 additions & 14 deletions config/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,61 @@
INVERTER_SERIAL = 123456789 # WiFi stick serial number
INVERTER_IP = "192.168.1.55" # IP address of inverter
INVERTER_PORT = 8899 # Port number
MQTT_SERVER = "192.168.1.20" # IP address of MQTT server
MQTT_PORT = 1883 # Port number of MQTT server
MQTT_TOPIC = "solis/METRICS" # MQTT topic to use
MQTT_USER = "foo" # MQTT auth user
MQTT_PASS = "bar" # MQTT auth password
CHECK_INTERVAL = 30 # How often to check(seconds), only applies when 'PROMETHEUS = False' otherwise uses Prometheus scrape interval
MQTT_KEEPALIVE = 60 # MQTT keepalive
PROMETHEUS = False # Enable Prometheus exporter
PROMETHEUS_PORT = 18000 # Port to use for Prometheus exporter
MODIFIED_METRICS = True # Enable modified metrics
DEBUG = False # Enable debugging, helpfull to diagnose problems
import json
import logging
import os


def _load_homeassistant_options():
options_path = os.environ.get("HASSIO_OPTIONS", "/data/options.json")
try:
with open(options_path, encoding="utf-8") as options_file:
return json.load(options_file)
except (FileNotFoundError, json.JSONDecodeError, OSError):
return {}


_OPTIONS = _load_homeassistant_options()


def _bool(value):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0

normalized = str(value).strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
logging.warning("Invalid boolean value %r; defaulting to False", value)
return False


def _setting(name, default, cast=None):
value = os.environ.get(name.upper(), _OPTIONS.get(name, default))
if value is None:
return default
if cast is None:
return value
# Never let a malformed env var / option crash the process at import time;
# log it and fall back to the known-good default instead.
try:
return cast(value)
except (ValueError, TypeError) as e:
logging.warning("Invalid value %r for %s (%s); using default %r", value, name, e, default)
return default


INVERTER_SERIAL = _setting("inverter_serial", 123456789, int) # WiFi stick serial number
INVERTER_IP = _setting("inverter_ip", "192.168.1.55") # IP address of inverter
INVERTER_PORT = _setting("inverter_port", 8899, int) # Port number
MQTT_SERVER = _setting("mqtt_server", "192.168.1.20") # IP address of MQTT server
MQTT_PORT = _setting("mqtt_port", 1883, int) # Port number of MQTT server
MQTT_TOPIC = _setting("mqtt_topic", "solis/METRICS") # MQTT topic to use
MQTT_USER = _setting("mqtt_user", "foo") # MQTT auth user
MQTT_PASS = _setting("mqtt_pass", "bar") # MQTT auth password
Comment on lines +54 to +55
CHECK_INTERVAL = max(1, _setting("check_interval", 30, int)) # How often to scrape(seconds); clamped to >= 1 so a bad value can't busy-loop or break sleep()
MQTT_KEEPALIVE = _setting("mqtt_keepalive", 60, int) # MQTT keepalive
PROMETHEUS = _setting("prometheus", False, _bool) # Enable Prometheus exporter
PROMETHEUS_PORT = _setting("prometheus_port", 18000, int) # Port to use for Prometheus exporter
MODIFIED_METRICS = _setting("modified_metrics", True, _bool) # Enable modified metrics
DEBUG = _setting("debug", False, _bool) # Enable debugging, helpfull to diagnose problems
Loading