Skip to content

Commit

Permalink
Add a validator for the config + create-passhash
Browse files Browse the repository at this point in the history
* JSON schame validator
* Create passhash flag
* Updated README
  • Loading branch information
mpepping committed Jan 23, 2022
1 parent 3cc4da5 commit 7f20917
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint paho_mqtt
pip install pylint paho_mqtt jsonschema
- name: Analysing the code with pylint
run: |
pylint `ls -R|grep .py$|xargs`
135 changes: 113 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,111 @@
Script to retrieve current Solar PV data from the Solarman API, and send Power (W) and Energy (kWh) metrics to a MQTT broker, for further use in home automation. Several PV vendors use the Solarman Smart platform for statistics. One example is the Trannergy PV converter.

```lang=bash
usage: run.py [-h] [-d] [-s] [-i INTERVAL] [-f FILE] [-v]
usage: run.py [-h] [-d] [-s] [-i INTERVAL] [-f FILE] [--validate] [--create-passhash CREATE_PASSHASH] [-v]
Collect data from Trannergy / Solarman API
optional arguments:
-h, --help show this help message and exit
-d, --daemon run as a service (default)
-s, --single single run and exit
-i INTERVAL, --interval INTERVAL run interval in seconds (default 300 sec.)
-f FILE, --file FILE config file (default ./config.json)
-v, --version show program's version number and exit
-h, --help show this help message and exit
-d, --daemon run as a service
-s, --single single run and exit
-i INTERVAL, --interval INTERVAL
run interval in seconds (default 300 sec.)
-f FILE, --file FILE config file (default ./config.json)
--validate validate config file and exit
--create-passhash CREATE_PASSHASH
create passhash from provided passwordand exit
-v, --version show program's version number and exit
```

## Usage

You can run this script as a Docker container or in Python 3. Either way a configuration file is required. See the sample `config.sample.json` file in this repository for reference. Also, a Solarman API appid+secret is required, which can be requested via <mailto:[email protected]>.
You can run this script as a Docker container or in Python 3. Either way a configuration file is required. See the sample `config.sample.json` file in this repository for reference. Also, a Solarman API appid+secret is required, which can be requested via <mailto:[email protected]>.

## Were do I get te required information for the config file?

Create a new config file by copying the sample file and filling in the required information.

The first part covers your account:

```lang=json
{
"name": "Trannergy",
"url": "api.solarmanpv.com",
"appid": "",
"secret": "",
"username": "",
"passhash": "",
}
```

* **name**: is free text to identify the platform.
* **url**: is the base URL of the API.
* **appid**: is the appid for the API (See Usage).
* **secret**: is the secret for the API (See Usage).
* **username**: is the username for the API (emailadres).
* **passhash**: is a sha256 hash of your password. This can be generated via `--create-passhash`.

The second part covers the PV inverter and logger ID's. These can be retrieved via the Solarman API.

```lang=json
{
"stationId": 123,
"inverterId": 456,
"loggerId": 789
}
```

* **stationId**: is the ID of the station. This is the value of `stationList[0].id`.

```lang=bash
curl --location --request POST 'https://api.solarmanpv.com//station/v1.0/list?language=en' \
--header 'Content-Type: application/json' \
--header 'Authorization: bearer TOKEN' \
--data-raw '{"size":20,"page":1}'
```

* **inverterId**: is the ID of the inverter. This is the value of `deviceListItems[0].deviceSn`

```lang=bash
curl --location --request POST 'https://api.solarmanpv.com//station/v1.0/device?language=en' \
--header 'Content-Type: application/json' \
--header 'Authorization: bearer TOKEN' \
--data-raw '{"size":10,"page":1,"stationId":1234567,"deviceType":"INVERTER"}'
```

* **loggerId**: is the ID of the logger. This is the value of `deviceListItems[0].deviceSn`.

```lang=bash
curl --location --request POST 'https://api.solarmanpv.com//station/v1.0/device?language=en' \
--header 'Content-Type: application/json' \
--header 'Authorization: bearer TOKEN' \
--data-raw '{"size":10,"page":1,"stationId":1234567,"deviceType":"COLLECTOR"}'
```

A bearer TOKEN to use in the requests above can be retrieved by adding your APPID, APPSECRET, USERNAME, PASSHASH in this request:

```lang=bash
curl --location --request POST 'https://api.solarmanpv.com//account/v1.0/token?appId=APPID&language=en' \
--header 'Content-Type: application/json' \
--data-raw '{
"appSecret": "APPSECRET",
"email": "USERNAME",
"password": "PASSHASH"
}'
```

The final section covers the MQTT broker, to where the metrics will be published.

```lang=json
{
"broker": "mqtt.example.com",
"port": 1883,
"topic": "solarman",
"username": "",
"password": ""
}
```

## MQTT topics

Expand Down Expand Up @@ -49,7 +138,7 @@ solarmanpv/inverter/deviceType
solarmanpv/inverter/attributes # contains all inverter datalist entries.
```

#### Attributes:
#### Attributes:

```lang=text
SN: XXXXXXXXXX
Expand Down Expand Up @@ -153,15 +242,15 @@ sensor:
state_class: measurement
```

Repeat for every station topic needed.
Repeat for every station topic needed.

```
```lang=yaml
sensor:
- platform: mqtt
name: "solarmanpv_inverter"
state_topic: "solarmanpv/inverter/deviceState"
json_attributes_topic: "solarmanpv/inverter/attributes"
- platform: mqtt
name: "solarmanpv_logger"
state_topic: "solarmanpv/logger/deviceState"
Expand All @@ -178,7 +267,7 @@ sensor:
'3' : 'Offline'} %}
{% set state = states.sensor.solarmanpv_inverter.state %}
{{ mapper[state] if state in mapper else 'Unknown' }}
- platform: template
sensors:
solarmanpv_logger_device_state:
Expand All @@ -190,12 +279,11 @@ sensor:
'3' : 'Offline'} %}
{% set state = states.sensor.solarmanpv_logger.state %}
{{ mapper[state] if state in mapper else 'Unknown' }}
```

### Templates

```lang=text
```lang=yaml
template:
- sensor:
- name: solarmanpv_inverter_dc_voltage_pv1
Expand All @@ -207,25 +295,25 @@ template:
unit_of_measurement: 'A'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'DC_Current_PV1') }}"
state_class: measurement
- sensor:
- name: solarmanpv_inverter_dc_power_pv1
unit_of_measurement: 'W'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'DC_Power_PV1') }}"
state_class: measurement
- sensor:
- name: solarmanpv_inverter_dc_power_pv1
unit_of_measurement: 'W'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'DC_Power_PV1') }}"
state_class: measurement
- sensor:
- name: solarmanpv_inverter_total_production_1
unit_of_measurement: 'kWh'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'Total_Production_1') }}"
state_class: total_increasing
- sensor:
- name: solarmanpv_inverter_daily_production_1
unit_of_measurement: 'kWh'
Expand All @@ -237,13 +325,13 @@ template:
unit_of_measurement: '°C'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'AC_Radiator_Temp') }}"
state_class: measurement
- sensor:
- name: solarmanpv_inverter_ac_voltage_1
unit_of_measurement: 'V'
state: "{{ state_attr('sensor.solarmanpv_inverter', 'AC_Voltage_1') }}"
state_class: measurement
- sensor:
- name: solarmanpv_inverter_ac_current_1
unit_of_measurement: 'A'
Expand All @@ -263,6 +351,10 @@ template:
![Screenshot](https://github.com/mpepping/solarman-mqtt/raw/main/doc/images/screenshot.png "Screenshot")
![Screenshot](https://github.com/mpepping/solarman-mqtt/raw/main/doc/images/screenshot_haenergy.png "Screenshot")

## Running

The easiest way to run is via a container. Current version is available at <https://github.com/mpepping/solarman-mqtt/pkgs/container/solarman-mqtt>

### Using Docker

Docker example to run this script every 5 minutes and providing a config file:
Expand Down Expand Up @@ -296,4 +388,3 @@ services:
### Using Python

Run `pip install -r requirements.txt` and start `python3 run.py`.

135 changes: 135 additions & 0 deletions helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Validate the JSON schema and contents used for the config file.
"""

import hashlib
import sys
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from jsonschema.exceptions import SchemaError

schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "solarman-mqtt-schema",
"type": "object",
"required": [
"name",
"url",
"appid",
"secret",
"username",
"passhash",
"stationId",
"inverterId",
"loggerId"
],
"properties": {
"name": {
"type": "string",
},
"url": {
"type": "string"
},
"appid": {
"type": "string",
"minLength": 15,
"maxLength": 15
},
"secret": {
"type": "string",
"minLength": 32,
"maxLength": 32
},
"username": {
"type": "string"
},
"passhash": {
"type": "string",
"minLength": 64,
"maxLength": 64
},
"stationId": {
"type": "number",
"minimum": 100000,
"maximum": 9999999
},
"inverterId": {
"type": "string",
"minLength": 10,
},
"loggerId": {
"type": "string",
"minLength": 10,
"maxLength": 10
},
"debug" : {
"type": "boolean",
"optional": True
},
"mqtt": {
"type": "object",
"properties": {
"broker": {
"type": "string",
},
"port": {
"type": "integer",
"minimum": 1024,
"maximum": 65535
},
"topic": {
"type": "string",
},
"username": {
"type": "string",
},
"password": {
"type": "string",
}
}
}
}
}

VALID = """
The provided config file is valid. This check validates if:
* The config file is a valid JSON file
* The config file contains the required keys
* The config file contains the correct types for the provided keys
Although that is not a guarantee that the contents are valid. If
you still have issues, please check all values and try again.
If you need any further help, please see:
<https://github.com/mpepping/solarman-mqtt>
"""

def check(config):
"""
Main
:return:
"""
try:
# validate(instance=json.load(open(config)), schema=schema)
validate(instance=config, schema=schema)
except ValidationError as err:
print(err.message)
sys.exit(1)
except SchemaError as err:
print(err.message)
sys.exit(1)


print(VALID)
sys.exit(0)


def hash_password(password):
"""
Hash the password
"""
encoded=password.encode('utf-8')
result = hashlib.sha256(encoded)
_hash = result.hexdigest()
return _hash
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
paho_mqtt==1.5.1
jsonschema==4.4.0
Loading

0 comments on commit 7f20917

Please sign in to comment.