First version
This commit is contained in:
parent
70d662a56f
commit
f1e62f5344
|
|
@ -0,0 +1,5 @@
|
|||
[flake8]
|
||||
ignore = E203, E266, E501, W503, F403, F401
|
||||
max-line-length = 79
|
||||
max-complexity = 18
|
||||
select = B,C,E,F,W,T4,B9
|
||||
|
|
@ -159,4 +159,3 @@ cython_debug/
|
|||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
name: Trim Trailing Whitespace
|
||||
description: This hook trims trailing whitespace.
|
||||
entry: trailing-whitespace-fixer
|
||||
language: python
|
||||
types: [ text ]
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: "5.12.0"
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: "6.0.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
args:
|
||||
- "--max-line-length=80"
|
||||
- repo: https://gitlab.com/emilv2/pre-commit-hooks
|
||||
rev: 0.0.7
|
||||
hooks:
|
||||
- id: check-config
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.10-slim-buster
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python3", "src/main.py"]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
version: "3"
|
||||
services:
|
||||
|
||||
huawei-solar:
|
||||
container_name: huawei-solar
|
||||
restart: unless-stopped
|
||||
image: huawei-solar-rtu:latest
|
||||
user: root
|
||||
devices:
|
||||
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||
environment:
|
||||
- INVERTER_PORT=/dev/ttyUSB0
|
||||
- MQTT_HOST=192.168.43.102
|
||||
- BROKER_PORT=1883
|
||||
- USE_CREDENTIALS=NO
|
||||
- USER_NAME=none
|
||||
- PASSWORD=none
|
||||
- MQTT_TOPIC=raspberryTopic
|
||||
- DATA_MODE=INVERTER # INVERTER or OFFLINE
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
[tool.poetry]
|
||||
name = "inverter_huawei"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Simon Milvert <simon@milvert.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
huawei-solar = "^2.2.4"
|
||||
paho-mqtt = "^1.6.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.2.1"
|
||||
black = "^23.1.0"
|
||||
flake8 = "^6.0.0"
|
||||
isort = "^5.12.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from huawei_solar import register_names as rn
|
||||
|
||||
|
||||
@dataclass
|
||||
class InverterData:
|
||||
"""Class for keeping track of an item in inventory."""
|
||||
|
||||
name: str
|
||||
unit: str
|
||||
value: int = 0
|
||||
|
||||
|
||||
class ReadRegister:
|
||||
inverter_reg_status = rn.DEVICE_STATUS
|
||||
|
||||
reg_to_read_measured = [
|
||||
rn.PV_01_VOLTAGE,
|
||||
rn.PV_02_VOLTAGE,
|
||||
rn.PV_01_CURRENT,
|
||||
rn.PV_02_CURRENT,
|
||||
rn.INPUT_POWER,
|
||||
rn.ACTIVE_POWER,
|
||||
rn.GRID_CURRENT,
|
||||
]
|
||||
|
||||
reg_to_read_calculated = [
|
||||
rn.DAY_ACTIVE_POWER_PEAK,
|
||||
rn.EFFICIENCY,
|
||||
rn.DAILY_YIELD_ENERGY,
|
||||
rn.ACCUMULATED_YIELD_ENERGY,
|
||||
rn.INTERNAL_TEMPERATURE,
|
||||
]
|
||||
|
||||
reg_to_read_status = [
|
||||
rn.FAULT_CODE,
|
||||
rn.STATE_1,
|
||||
rn.STATE_2,
|
||||
rn.STATE_3,
|
||||
rn.ALARM_1,
|
||||
rn.ALARM_2,
|
||||
rn.ALARM_3,
|
||||
]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from src.data import InverterData
|
||||
from src.log import log_info
|
||||
|
||||
|
||||
async def get_data(client, register, slave_id):
|
||||
result = await client.get(register, slave_id)
|
||||
log_info(f"{register}: {result.value}")
|
||||
return InverterData(value=result.value, unit=result.unit, name=register)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import random
|
||||
|
||||
from src.data import InverterData, ReadRegister
|
||||
from src.log import log_info
|
||||
|
||||
POSSIBLE_SAMPLE_UNITS = [
|
||||
"EXAMPLE_UNIT_A",
|
||||
"EXAMPLE_UNIT_B",
|
||||
"EXAMPLE_UNIT_C",
|
||||
"EXAMPLE_UNIT_D",
|
||||
"EXAMPLE_UNIT_E",
|
||||
"EXAMPLE_UNIT_F",
|
||||
]
|
||||
|
||||
|
||||
async def get_dummy_data() -> InverterData:
|
||||
log_info("Generate dummy data")
|
||||
value = random.randint(0, 100)
|
||||
name = random.choice(ReadRegister.reg_to_read_measured)
|
||||
unit = random.choice(POSSIBLE_SAMPLE_UNITS)
|
||||
sample_reg = InverterData(value=value, unit=unit, name=name)
|
||||
return sample_reg
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import logging
|
||||
|
||||
FORMAT = (
|
||||
"%(asctime)-15s %(threadName)-15s "
|
||||
"%(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s"
|
||||
)
|
||||
logging.basicConfig(format=FORMAT)
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def log_info(message):
|
||||
log.info(message)
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
|
||||
from huawei_solar import AsyncHuaweiSolar
|
||||
from huawei_solar import register_values as rv
|
||||
|
||||
from src.data import InverterData, ReadRegister
|
||||
from src.inverter import get_data
|
||||
from src.inverter_dummy import get_dummy_data
|
||||
from src.log import log_info
|
||||
from src.mqtt import connect
|
||||
|
||||
DATA_MODE = os.getenv("DATA_MODE", "dummy")
|
||||
MQTT_TOPIC = os.getenv("MQTT_TOPIC", "inverter")
|
||||
|
||||
REQUEST_INTERVAL = 60
|
||||
slave_id = os.getenv("INVERTER_ID", 1)
|
||||
port = os.getenv("INVERTER_PORT", 502)
|
||||
host = os.getenv("INVERTER_HOST", "10.0.2.20")
|
||||
|
||||
|
||||
async def get_inverter_client() -> (InverterData, str):
|
||||
inverter_client = await AsyncHuaweiSolar.create(host, port, slave_id)
|
||||
log_info("-- INVERTER -- Connected to Inverter")
|
||||
status = await inverter_client.get(
|
||||
ReadRegister.inverter_reg_status, slave_id
|
||||
)
|
||||
|
||||
log_info(f"-- INVERTER -- Status: {status}")
|
||||
return inverter_client, status
|
||||
|
||||
|
||||
async def main():
|
||||
inverter_client, status = await get_inverter_client()
|
||||
count = 0
|
||||
while True:
|
||||
if status.value == rv.DEVICE_STATUS_DEFINITIONS.get(
|
||||
0x0200
|
||||
): # Inverter is active, Get data
|
||||
# Get measured values
|
||||
for register in ReadRegister.reg_to_read_measured:
|
||||
result = await get_inverter_data(
|
||||
inverter_client, register, slave_id
|
||||
)
|
||||
send_data(
|
||||
mqtt_client,
|
||||
format_data_to_serialized_json(result),
|
||||
f"measure/{result.name}",
|
||||
)
|
||||
|
||||
# Get calculated values
|
||||
for register in ReadRegister.reg_to_read_calculated:
|
||||
result = await get_inverter_data(
|
||||
inverter_client, register, slave_id
|
||||
)
|
||||
send_data(
|
||||
mqtt_client,
|
||||
format_data_to_serialized_json(result),
|
||||
f"calculated/{result.name}",
|
||||
)
|
||||
|
||||
if count == 5:
|
||||
for register in ReadRegister.reg_to_read_status:
|
||||
result = await get_inverter_data(
|
||||
inverter_client, register, slave_id
|
||||
)
|
||||
send_data(
|
||||
mqtt_client,
|
||||
format_data_to_serialized_json(result),
|
||||
f"status/{result.name}",
|
||||
)
|
||||
count = 0
|
||||
count += 1
|
||||
await asyncio.sleep(REQUEST_INTERVAL)
|
||||
|
||||
|
||||
def send_data(client, storable_data, topic=""):
|
||||
try:
|
||||
log_info(
|
||||
f"Sending data\n topic: {MQTT_TOPIC + '/' + topic} \n"
|
||||
f"msg: {storable_data}"
|
||||
)
|
||||
client.publish(
|
||||
MQTT_TOPIC + "/" + topic,
|
||||
payload=storable_data,
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
except Exception:
|
||||
log_info(f"ERROR PUBLISHING DATA TO MQTT BROKER. \n{Exception}")
|
||||
|
||||
|
||||
def format_data_to_serialized_json(info: InverterData) -> str:
|
||||
data = {"value": info.value, "name": info.name, "unit": info.unit}
|
||||
json_obj = json.dumps(data)
|
||||
log_info(f"json_obj: {json_obj}, type: {type(json_obj)}")
|
||||
return json_obj
|
||||
|
||||
|
||||
async def get_inverter_data(
|
||||
inverter_client: AsyncHuaweiSolar, register: str, slave_id: int
|
||||
) -> InverterData:
|
||||
if DATA_MODE == "INVERTER":
|
||||
data = await get_data(inverter_client, register, slave_id)
|
||||
return data
|
||||
else:
|
||||
data = await get_dummy_data()
|
||||
return data
|
||||
|
||||
|
||||
log_info("| === START === |")
|
||||
mqtt_client = connect()
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
try:
|
||||
asyncio.ensure_future(main())
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
log_info("| === END === |")
|
||||
loop.close()
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from src.log import log_info
|
||||
|
||||
mqtt_host = os.getenv("MQTT_HOST", "10.0.0.3")
|
||||
broker_port = os.getenv("BROKER_PORT", "1883")
|
||||
have_credentials = os.getenv("USE_CREDENTIALS", "NO")
|
||||
user_name = os.getenv("USER_NAME", "")
|
||||
password = os.getenv("PASSWORD", "")
|
||||
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
if rc == 0:
|
||||
client.connected_flag = True
|
||||
log_info("MQTT OK!")
|
||||
else:
|
||||
log_info(f"MQTT FAILURE. ERROR CODE: {rc}")
|
||||
|
||||
|
||||
def setup_mqtt():
|
||||
mqtt.Client.connected_flag = False
|
||||
mqtt_client = mqtt.Client()
|
||||
mqtt_client.on_connect = on_connect
|
||||
mqtt_client.loop_start()
|
||||
log_info("Connecting to MQTT broker: " + mqtt_host)
|
||||
log_info("Port: " + broker_port)
|
||||
if have_credentials == "YES":
|
||||
mqtt_client.username_pw_set(username=user_name, password=password)
|
||||
mqtt_client.connect(mqtt_host, int(broker_port), 60)
|
||||
while not mqtt_client.connected_flag:
|
||||
log_info("...")
|
||||
time.sleep(1)
|
||||
log_info("START MODBUS...")
|
||||
return mqtt_client
|
||||
|
||||
|
||||
def connect():
|
||||
mqtt_client = setup_mqtt()
|
||||
return mqtt_client
|
||||
Loading…
Reference in New Issue