Functional updates, documentation updates
- Add BSD-3-Clause license - Update documentation - Add support for timestamp subtopics for monitoring purposes - Add disclaimer, fix typos
This commit is contained in:
parent
b3dea6acfd
commit
d150cba3b3
4 changed files with 116 additions and 27 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/fronius2mqtt.conf
|
27
LICENSE.md
Normal file
27
LICENSE.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2016-2024, Gerrit Beine
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of fronius2mqtt nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
82
README.md
82
README.md
|
@ -5,40 +5,88 @@ A Fronius HTTP API to MQTT bridge
|
||||||
Attention: This is a complete rewrite of the bridge.
|
Attention: This is a complete rewrite of the bridge.
|
||||||
While the old fronius2mqtt brigde used polling the inverters to fetch the data, the new version make use of the push feature provided by Fronius Symo.
|
While the old fronius2mqtt brigde used polling the inverters to fetch the data, the new version make use of the push feature provided by Fronius Symo.
|
||||||
|
|
||||||
The daemon offers no HTTP endpoints which can be configured in the configuration interface of each inverter.
|
The daemon offers now HTTP endpoints which can be configured in the configuration interface of each inverter.
|
||||||
It forwards the data directly to MQTT.
|
It forwards the data directly to MQTT.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Installation using Docker
|
### Installation using Docker
|
||||||
|
|
||||||
|
```
|
||||||
docker run -it --rm --name fronius2mqtt -v fronius2mqtt.conf:/etc/fronius2mqtt.conf docker.io/gbeine/fronius2mqtt
|
docker run -it --rm --name fronius2mqtt -v fronius2mqtt.conf:/etc/fronius2mqtt.conf docker.io/gbeine/fronius2mqtt
|
||||||
|
```
|
||||||
|
|
||||||
### Native installation with Python venv
|
### Native installation with Python venv
|
||||||
|
|
||||||
- clone the git repository
|
- clone the git repository
|
||||||
- ensure to have Python 3 with venv installed
|
- ensure to have Python 3 with venv installed
|
||||||
- run the ```install``` script in the local directory
|
- run the `install` script in the local directory
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
### Device configuration
|
||||||
|
|
||||||
|
On the settings page for you Fronius device, select 'Push Service'.
|
||||||
|
|
||||||
|
Define an identifier for your device, e.g. `fronius1`, and add the following values:
|
||||||
|
|
||||||
|
| Field | Explanation |
|
||||||
|
|------------------|------------------------------------------------------------------------------|
|
||||||
|
| Name | As you like, only relevant on the device side |
|
||||||
|
| Data Format | Select one from the table below, choose HTTP Post |
|
||||||
|
| Interval | 10 sec, or use the default |
|
||||||
|
| Activate | Select |
|
||||||
|
| Server:Port | The hostname and port of your HTTP server, e.g. `smarthome.example.org:8080` |
|
||||||
|
| Upload file name | /path/identifier, e.g. `/current_data_inverter/fronius1` |
|
||||||
|
| Login | Not needed unless you use an authentication proxy |
|
||||||
|
| Proxy | Not needed unless you use a dedicated proxy server |
|
||||||
|
|
||||||
|
| Data format | URL path |
|
||||||
|
|-------------------------------------------------|---------------------------------------------------|
|
||||||
|
| Datamanager IO States | `/datamanager_io_states/<device identifier>` |
|
||||||
|
| SolarAPI v1 - CurrentData - Inverter | `/current_data_inverter/<device identifier>` |
|
||||||
|
| SolarAPI v1 - CurrentData - Meter | `/current_data_meter/<device identifier>` |
|
||||||
|
| SolarAPI v1 - CurrentData - Powerflow | `/current_data_powerflow/<device identifier>` |
|
||||||
|
| SolarAPI v1 - CurrentData - SensorCard | `/current_data_sensorcard/<device identifier>` |
|
||||||
|
| SolarAPI v1 - CurrentData - StringControl | `/current_data_stringcontrol/<device identifier>` |
|
||||||
|
| SolarAPI v1 - Logdata - Data | `/logdata_data/<device identifier>` |
|
||||||
|
| SolarAPI v1 - Logdata - Errors and Events | `/logdata_errors_and_events/<device identifier>` |
|
||||||
|
| SunSpec Datalogger v1.0b - inverter float model | - not yet implemented - |
|
||||||
|
| SunSpec Datalogger v1.2 - meter model | - not yet implemented - |
|
||||||
|
|
||||||
|
### fronius2mqtt.conf
|
||||||
|
|
||||||
Each configuration option is also available as command line argument.
|
Each configuration option is also available as command line argument.
|
||||||
|
|
||||||
- copy ```fronius2mqtt.conf.example```
|
- copy ```fronius2mqtt.conf.example```
|
||||||
- configure as you like
|
- configure as you like
|
||||||
|
|
||||||
| option | default | arguments | comment |
|
| option | default | arguments | comment |
|
||||||
|------------------|--------------------------|---------------------|----------------------------------------------------------------------------------------|
|
|--------------------|--------------------------|-------------------------|----------------------------------------------------------------------------------------|
|
||||||
| mqtt_host | 'localhost' | -m, --mqtt_host | The hostname of the MQTT server. |
|
| `mqtt_host` | 'localhost' | `-m`, `--mqtt_host` | The hostname of the MQTT server. |
|
||||||
| mqtt_port | 1883 | --mqtt_port | The port of the MQTT server. |
|
| `mqtt_port` | 1883 | `--mqtt_port` | The port of the MQTT server. |
|
||||||
| mqtt_keepalive | 30 | --mqtt_keepalive | The keep alive interval for the MQTT server connection in seconds. |
|
| `mqtt_keepalive` | 30 | `--mqtt_keepalive` | The keep alive interval for the MQTT server connection in seconds. |
|
||||||
| mqtt_clientid | 'fronius2mqtt' | --mqtt_clientid | The clientid to send to the MQTT server. |
|
| `mqtt_clientid` | 'fronius2mqtt' | `--mqtt_clientid` | The clientid to send to the MQTT server. |
|
||||||
| mqtt_user | - | -u, --mqtt_user | The username for the MQTT server connection. |
|
| `mqtt_user` | - | `-u`, `--mqtt_user` | The username for the MQTT server connection. |
|
||||||
| mqtt_password | - | -p, --mqtt_password | The password for the MQTT server connection. |
|
| `mqtt_password` | - | `-p`, `--mqtt_password` | The password for the MQTT server connection. |
|
||||||
| mqtt_topic | 'fronius' | -t, --mqtt_topic | The topic to publish MQTT message. |
|
| `mqtt_topic` | 'fronius' | `-t`, `--mqtt_topic` | The topic to publish MQTT message. |
|
||||||
| mqtt_tls_version | 'TLSv1.2' | --mqtt_tls_version | The TLS version to use for MQTT. One of TLSv1, TLSv1.1, TLSv1.2. |
|
| `mqtt_tls_version` | 'TLSv1.2' | `--mqtt_tls_version` | The TLS version to use for MQTT. One of TLSv1, TLSv1.1, TLSv1.2. |
|
||||||
| mqtt_verify_mode | 'CERT_REQUIRED' | --mqtt_verify_mode | The SSL certificate verification mode. One of CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED. |
|
| `mqtt_verify_mode` | 'CERT_REQUIRED' | `--mqtt_verify_mode` | The SSL certificate verification mode. One of CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED. |
|
||||||
| http_host | 'localhost' | --http_host | The address of the HTTP server. |
|
| `http_host` | 'localhost' | `--http_host` | The address of the HTTP server. |
|
||||||
| http_port | 8080 | --http_port | The port of the HTTP server. |
|
| `http_port` | 8080 | ``--http_port`` | The port of the HTTP server. |
|
||||||
| verbose | - | -v, --verbose | Be verbose while running. |
|
| `timestamp` | - | `-z`, `--timestamp` | Publish timestamps for all topics, e.g. for monitoring purposes. |
|
||||||
| - | '/etc/fronius2mqtt.conf' | -c, --config | The path to the config file. |
|
| `verbose` | - | `-v`, `--verbose` | Be verbose while running. |
|
||||||
|
| - | '/etc/fronius2mqtt.conf' | `-c`, `--config` | The path to the config file. |
|
||||||
|
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
''Attention:'' Please be aware that the data is published over plain HTTP by this solution.
|
||||||
|
Use with care ond only if you know what you're doing.
|
||||||
|
|
||||||
|
## Future plans
|
||||||
|
|
||||||
|
- Support for HTTPS
|
||||||
|
- Authentication support
|
||||||
|
- Support for new Fronius API
|
||||||
|
|
||||||
|
|
33
fronius2mqtt
33
fronius2mqtt
|
@ -5,6 +5,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import ssl
|
import ssl
|
||||||
|
import time
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
from bottle import request, route, post, run
|
from bottle import request, route, post, run
|
||||||
|
@ -38,6 +39,12 @@ def extract_request_data():
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def publish(topic, payload):
|
||||||
|
mqtt_client.publish(topic, payload)
|
||||||
|
if daemon_args.timestamp:
|
||||||
|
mqtt_client.publish("{}/timestamp".format(topic), time.time(), retain=True)
|
||||||
|
|
||||||
|
|
||||||
@route('/')
|
@route('/')
|
||||||
def index():
|
def index():
|
||||||
return "Hello World!<br/>This is the fronius2mqtt daemon by Gerrit Beine"
|
return "Hello World!<br/>This is the fronius2mqtt daemon by Gerrit Beine"
|
||||||
|
@ -52,19 +59,19 @@ def current_data_inverter(device):
|
||||||
if 'PAC' in data['Body'] and 'Values' in data['Body']['PAC']:
|
if 'PAC' in data['Body'] and 'Values' in data['Body']['PAC']:
|
||||||
for k, v in data['Body']['PAC']['Values'].items():
|
for k, v in data['Body']['PAC']['Values'].items():
|
||||||
topic = "{}/pac/{}".format(topic_base, k)
|
topic = "{}/pac/{}".format(topic_base, k)
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
if 'DAY_ENERGY' in data['Body'] and 'Values' in data['Body']['DAY_ENERGY']:
|
if 'DAY_ENERGY' in data['Body'] and 'Values' in data['Body']['DAY_ENERGY']:
|
||||||
for k, v in data['Body']['DAY_ENERGY']['Values'].items():
|
for k, v in data['Body']['DAY_ENERGY']['Values'].items():
|
||||||
topic = "{}/day_energy/{}".format(topic_base, k)
|
topic = "{}/day_energy/{}".format(topic_base, k)
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
if 'YEAR_ENERGY' in data['Body'] and 'Values' in data['Body']['YEAR_ENERGY']:
|
if 'YEAR_ENERGY' in data['Body'] and 'Values' in data['Body']['YEAR_ENERGY']:
|
||||||
for k, v in data['Body']['YEAR_ENERGY']['Values'].items():
|
for k, v in data['Body']['YEAR_ENERGY']['Values'].items():
|
||||||
topic = "{}/year_energy/{}".format(topic_base, k)
|
topic = "{}/year_energy/{}".format(topic_base, k)
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
if 'TOTAL_ENERGY' in data['Body'] and 'Values' in data['Body']['TOTAL_ENERGY']:
|
if 'TOTAL_ENERGY' in data['Body'] and 'Values' in data['Body']['TOTAL_ENERGY']:
|
||||||
for k, v in data['Body']['TOTAL_ENERGY']['Values'].items():
|
for k, v in data['Body']['TOTAL_ENERGY']['Values'].items():
|
||||||
topic = "{}/total_energy/{}".format(topic_base, k)
|
topic = "{}/total_energy/{}".format(topic_base, k)
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +85,7 @@ def current_data_meter(device):
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if v is not None and type(v) in [int, float, str] :
|
if v is not None and type(v) in [int, float, str] :
|
||||||
topic = "{}/{}/{}".format(topic_base, m, k.lower())
|
topic = "{}/{}/{}".format(topic_base, m, k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,13 +99,13 @@ def current_data_powerflow(device):
|
||||||
for k, v in data['Body']['Site'].items():
|
for k, v in data['Body']['Site'].items():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
topic = "{}/site/{}".format(topic_base, k.lower())
|
topic = "{}/site/{}".format(topic_base, k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
if 'Inverters' in data['Body']:
|
if 'Inverters' in data['Body']:
|
||||||
for i, d in data['Body']['Inverters'].items():
|
for i, d in data['Body']['Inverters'].items():
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
topic = "{}/{}/{}".format(topic_base, i, k.lower())
|
topic = "{}/{}/{}".format(topic_base, i, k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,14 +120,14 @@ def current_data_storages(device):
|
||||||
for k, v in d['Controller'].items():
|
for k, v in d['Controller'].items():
|
||||||
if v is not None and type(v) in [int, float, str] :
|
if v is not None and type(v) in [int, float, str] :
|
||||||
topic = "{}/{}/{}".format(topic_base, s, k.lower())
|
topic = "{}/{}/{}".format(topic_base, s, k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
if 'Modules' in d:
|
if 'Modules' in d:
|
||||||
for m in d['Modules']:
|
for m in d['Modules']:
|
||||||
serial = m['Details']['Serial']
|
serial = m['Details']['Serial']
|
||||||
for k, v in m.items():
|
for k, v in m.items():
|
||||||
if v is not None and type(v) in [int, float, str] :
|
if v is not None and type(v) in [int, float, str] :
|
||||||
topic = "{}/{}/{}/{}".format(topic_base, s, serial.lower(), k.lower())
|
topic = "{}/{}/{}/{}".format(topic_base, s, serial.lower(), k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,7 +159,7 @@ def datamanager_io_states(device):
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if v is not None:
|
if v is not None:
|
||||||
topic = "{}/{}/{}".format(topic_base, p.replace(' ', '_'), k.lower())
|
topic = "{}/{}/{}".format(topic_base, p.replace(' ', '_'), k.lower())
|
||||||
mqtt_client.publish(topic, v)
|
publish(topic, v)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
@ -243,6 +250,10 @@ def parse_args():
|
||||||
parser.add_argument('-c', '--config', type=str,
|
parser.add_argument('-c', '--config', type=str,
|
||||||
default='/etc/fronius2mqtt.conf',
|
default='/etc/fronius2mqtt.conf',
|
||||||
help='The path to the config file. Default is /etc/fronius2mqtt.conf')
|
help='The path to the config file. Default is /etc/fronius2mqtt.conf')
|
||||||
|
parser.add_argument('-z', '--timestamp',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Publish timestamps for all topics, e.g. for monitoring purposes.')
|
||||||
parser.add_argument('-v', '--verbose',
|
parser.add_argument('-v', '--verbose',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
@ -290,6 +301,8 @@ def parse_config():
|
||||||
daemon_args.http_host = data['http_host']
|
daemon_args.http_host = data['http_host']
|
||||||
if 'http_port' in data:
|
if 'http_port' in data:
|
||||||
daemon_args.http_port = int(data['http_port'])
|
daemon_args.http_port = int(data['http_port'])
|
||||||
|
if 'timestamp' in data:
|
||||||
|
daemon_args.timestamp = data['timestamp']
|
||||||
if 'verbose' in data:
|
if 'verbose' in data:
|
||||||
daemon_args.verbose = data['verbose']
|
daemon_args.verbose = data['verbose']
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue