Compare commits
1 Commits
renovate/a
...
v0.10.0-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ff17e600 |
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
- GEN3PLUS: add client_mode connection on port 8899 [#117](https://github.com/s-allius/tsun-gen3-proxy/issues/117)
|
||||||
|
|
||||||
## [0.9.0] - 2024-07-01
|
## [0.9.0] - 2024-07-01
|
||||||
|
|
||||||
- fix exception in MODBUS timeout callback
|
- fix exception in MODBUS timeout callback
|
||||||
|
|||||||
11
app/build.sh
11
app/build.sh
@@ -21,11 +21,11 @@ IMAGE=tsun-gen3-proxy
|
|||||||
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
if [[ $1 == debug ]] || [[ $1 == dev ]] ;then
|
||||||
IMAGE=docker.io/sallius/${IMAGE}
|
IMAGE=docker.io/sallius/${IMAGE}
|
||||||
VERSION=${VERSION}-$1
|
VERSION=${VERSION}-$1
|
||||||
elif [[ $1 == rc ]] || [[ $1 == rel ]];then
|
elif [[ $1 == rc ]] || [[ $1 == rel ]] || [[ $1 == preview ]] ;then
|
||||||
IMAGE=ghcr.io/s-allius/${IMAGE}
|
IMAGE=ghcr.io/s-allius/${IMAGE}
|
||||||
else
|
else
|
||||||
echo argument missing!
|
echo argument missing!
|
||||||
echo try: $0 '[debug|dev|rc|rel]'
|
echo try: $0 '[debug|dev|preview|rc|rel]'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@ docker build --build-arg "VERSION=${VERSION}" --build-arg environment=dev --buil
|
|||||||
elif [[ $1 == dev ]];then
|
elif [[ $1 == dev ]];then
|
||||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:dev app
|
||||||
|
|
||||||
|
elif [[ $1 == preview ]];then
|
||||||
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:preview -t ${IMAGE}:${VERSION} app
|
||||||
|
echo 'login to ghcr.io'
|
||||||
|
echo $GHCR_TOKEN | docker login ghcr.io -u s-allius --password-stdin
|
||||||
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:preview
|
||||||
|
docker push -q ghcr.io/s-allius/tsun-gen3-proxy:${VERSION}
|
||||||
|
|
||||||
elif [[ $1 == rc ]];then
|
elif [[ $1 == rc ]];then
|
||||||
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
docker build --build-arg "VERSION=${VERSION}" --build-arg environment=production --label "org.opencontainers.image.created=${BUILD_DATE}" --label "org.opencontainers.image.version=${VERSION}" --label "org.opencontainers.image.revision=${BRANCH}" -t ${IMAGE}:rc -t ${IMAGE}:${VERSION} app
|
||||||
echo 'login to ghcr.io'
|
echo 'login to ghcr.io'
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ inverters.allow_all = true # allow inverters, even if we have no inverter mapp
|
|||||||
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
monitor_sn = 2000000000 # The "Monitoring SN:" can be found on a sticker enclosed with the inverter
|
||||||
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
#node_id = '' # Optional, MQTT replacement for inverters serial number
|
||||||
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
#suggested_area = '' # Optional, suggested installation place for home-assistant
|
||||||
|
#client_mode = {host = '192.168.0.1', port = 8899}
|
||||||
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv1 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv2 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
#pv3 = {type = 'RSM40-8-410M', manufacturer = 'Risen'} # Optional, PV module descr
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ class Config():
|
|||||||
Use(lambda s: s + '/'
|
Use(lambda s: s + '/'
|
||||||
if len(s) > 0 and
|
if len(s) > 0 and
|
||||||
s[-1] != '/' else s)),
|
s[-1] != '/' else s)),
|
||||||
|
Optional('client_mode'): {
|
||||||
|
'host': Use(str),
|
||||||
|
Optional('port', default=8899):
|
||||||
|
And(Use(int), lambda n: 1024 <= n <= 65535)
|
||||||
|
},
|
||||||
Optional('suggested_area', default=""): Use(str),
|
Optional('suggested_area', default=""): Use(str),
|
||||||
Optional('pv1'): {
|
Optional('pv1'): {
|
||||||
Optional('type'): Use(str),
|
Optional('type'): Use(str),
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class SolarmanV5(Message):
|
|||||||
self.db = InfosG3P()
|
self.db = InfosG3P()
|
||||||
self.time_ofs = 0
|
self.time_ofs = 0
|
||||||
self.forward_at_cmd_resp = False
|
self.forward_at_cmd_resp = False
|
||||||
|
self.no_forwarding = False
|
||||||
|
'''not allowed to connect to TSUN cloud by connection type'''
|
||||||
self.switch = {
|
self.switch = {
|
||||||
|
|
||||||
0x4210: self.msg_data_ind, # real time data
|
0x4210: self.msg_data_ind, # real time data
|
||||||
@@ -143,6 +145,19 @@ class SolarmanV5(Message):
|
|||||||
self.mb_timer.close()
|
self.mb_timer.close()
|
||||||
super().close()
|
super().close()
|
||||||
|
|
||||||
|
async def send_start_cmd(self, snr: int):
|
||||||
|
self.no_forwarding = True
|
||||||
|
self.snr = snr
|
||||||
|
self.__set_serial_no(snr)
|
||||||
|
|
||||||
|
self.__send_ack_rsp(0x1710, ftype=0)
|
||||||
|
await self.async_write('Send Start Command:')
|
||||||
|
self._send_buffer = bytearray(0)
|
||||||
|
|
||||||
|
self.state = State.up
|
||||||
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 64, logging.INFO)
|
||||||
|
self.mb_timer.start(self.MB_START_TIMEOUT)
|
||||||
|
|
||||||
def __set_serial_no(self, snr: int):
|
def __set_serial_no(self, snr: int):
|
||||||
serial_no = str(snr)
|
serial_no = str(snr)
|
||||||
if self.unique_id == serial_no:
|
if self.unique_id == serial_no:
|
||||||
@@ -198,6 +213,8 @@ class SolarmanV5(Message):
|
|||||||
return 0 # wait 0s before sending a response
|
return 0 # wait 0s before sending a response
|
||||||
|
|
||||||
def forward(self, buffer, buflen) -> None:
|
def forward(self, buffer, buflen) -> None:
|
||||||
|
if self.no_forwarding:
|
||||||
|
return
|
||||||
tsun = Config.get('solarman')
|
tsun = Config.get('solarman')
|
||||||
if tsun['enabled']:
|
if tsun['enabled']:
|
||||||
self._forward_buffer = buffer[:buflen]
|
self._forward_buffer = buffer[:buflen]
|
||||||
@@ -380,11 +397,11 @@ class SolarmanV5(Message):
|
|||||||
def mb_timout_cb(self, exp_cnt):
|
def mb_timout_cb(self, exp_cnt):
|
||||||
self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
|
self.mb_timer.start(self.MB_REGULAR_TIMEOUT)
|
||||||
|
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x3008, 21, logging.DEBUG)
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x3000, 48, logging.DEBUG)
|
||||||
|
|
||||||
if 0 == (exp_cnt % 30):
|
if 0 == (exp_cnt % 30):
|
||||||
# logging.info("Regular Modbus Status request")
|
# logging.info("Regular Modbus Status request")
|
||||||
self._send_modbus_cmd(Modbus.READ_REGS, 0x2007, 2, logging.DEBUG)
|
self._send_modbus_cmd(Modbus.READ_REGS, 0x2000, 64, logging.DEBUG)
|
||||||
|
|
||||||
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
def at_cmd_forbidden(self, cmd: str, connection: str) -> bool:
|
||||||
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
return not cmd.startswith(tuple(self.at_acl[connection]['allow'])) or \
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class Message(metaclass=IterRegistry):
|
|||||||
self._forward_buffer = bytearray(0)
|
self._forward_buffer = bytearray(0)
|
||||||
self.new_data = {}
|
self.new_data = {}
|
||||||
self.state = State.init
|
self.state = State.init
|
||||||
|
self.shutdown_started = False
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Empty methods, that have to be implemented in any child class which
|
Empty methods, that have to be implemented in any child class which
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ class Modbus():
|
|||||||
__crc_tab = []
|
__crc_tab = []
|
||||||
map = {
|
map = {
|
||||||
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
0x2007: {'reg': Register.MAX_DESIGNED_POWER, 'fmt': '!H', 'ratio': 1}, # noqa: E501
|
||||||
# 0x????: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
0x203e: {'reg': Register.NO_INPUTS, 'fmt': '!H', 'ratio': 1/256}, # noqa: E501
|
||||||
|
|
||||||
|
0x3000: {'reg': Register.INVERTER_STATUS, 'fmt': '!H'}, # noqa: E501
|
||||||
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
0x3008: {'reg': Register.VERSION, 'fmt': '!H', 'eval': "f'V{(result>>12)}.{(result>>8)&0xf}.{(result>>4)&0xf}{result&0xf}'"}, # noqa: E501
|
||||||
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
0x3009: {'reg': Register.GRID_VOLTAGE, 'fmt': '!H', 'ratio': 0.1}, # noqa: E501
|
||||||
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
0x300a: {'reg': Register.GRID_CURRENT, 'fmt': '!H', 'ratio': 0.01}, # noqa: E501
|
||||||
|
|||||||
70
app/src/modbus_tcp.py
Normal file
70
app/src/modbus_tcp.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
# import gc
|
||||||
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
|
|
||||||
|
logger = logging.getLogger('conn')
|
||||||
|
|
||||||
|
|
||||||
|
class ModbusConn():
|
||||||
|
def __init__(self, host, port):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.addr = (host, port)
|
||||||
|
self.stream = None
|
||||||
|
|
||||||
|
async def __aenter__(self) -> 'InverterG3P':
|
||||||
|
'''Establish a client connection to the TSUN cloud'''
|
||||||
|
connection = asyncio.open_connection(self.host, self.port)
|
||||||
|
reader, writer = await connection
|
||||||
|
self.stream = InverterG3P(reader, writer, self.addr)
|
||||||
|
logging.info(f'[{self.stream.node_id}:{self.stream.conn_no}] '
|
||||||
|
f'Connected to {self.addr}')
|
||||||
|
self.stream.inc_counter('Inverter_Cnt')
|
||||||
|
return self.stream
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
self.stream.dec_counter('Inverter_Cnt')
|
||||||
|
|
||||||
|
|
||||||
|
class ModbusTcp():
|
||||||
|
|
||||||
|
def __init__(self, loop) -> None:
|
||||||
|
inverters = Config.get('inverters')
|
||||||
|
# logging.info(f'Inverters: {inverters}')
|
||||||
|
|
||||||
|
for inv in inverters.values():
|
||||||
|
if (type(inv) is dict
|
||||||
|
and 'monitor_sn' in inv
|
||||||
|
and 'client_mode' in inv):
|
||||||
|
client = inv['client_mode']
|
||||||
|
# logging.info(f"SerialNo:{inv['monitor_sn']} host:{client['host']} port:{client['port']}") # noqa: E501
|
||||||
|
loop.create_task(self.modbus_loop(client['host'],
|
||||||
|
client['port'],
|
||||||
|
inv['monitor_sn']))
|
||||||
|
|
||||||
|
async def modbus_loop(self, host, port, snr: int) -> None:
|
||||||
|
'''Loop for receiving messages from the TSUN cloud (client-side)'''
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with ModbusConn(host, port) as stream:
|
||||||
|
await stream.send_start_cmd(snr)
|
||||||
|
await stream.loop()
|
||||||
|
logger.info(f'[{stream.node_id}:{stream.conn_no}] '
|
||||||
|
f'Connection closed - Shutdown: '
|
||||||
|
f'{stream.shutdown_started}')
|
||||||
|
if stream.shutdown_started:
|
||||||
|
return
|
||||||
|
|
||||||
|
except (ConnectionRefusedError, TimeoutError) as error:
|
||||||
|
logging.info(f'{error}')
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.error(
|
||||||
|
f"ModbusTcpCreate: Exception for {(host,port)}:\n"
|
||||||
|
f"{traceback.format_exc()}")
|
||||||
|
|
||||||
|
await asyncio.sleep(10)
|
||||||
@@ -11,6 +11,7 @@ from gen3.inverter_g3 import InverterG3
|
|||||||
from gen3plus.inverter_g3p import InverterG3P
|
from gen3plus.inverter_g3p import InverterG3P
|
||||||
from scheduler import Schedule
|
from scheduler import Schedule
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from modbus_tcp import ModbusTcp
|
||||||
|
|
||||||
routes = web.RouteTableDef()
|
routes = web.RouteTableDef()
|
||||||
proxy_is_up = False
|
proxy_is_up = False
|
||||||
@@ -94,6 +95,7 @@ async def handle_shutdown(web_task):
|
|||||||
# first, disc all open TCP connections gracefully
|
# first, disc all open TCP connections gracefully
|
||||||
#
|
#
|
||||||
for stream in Message:
|
for stream in Message:
|
||||||
|
stream.shutdown_started = True
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(stream.disc(), 2)
|
await asyncio.wait_for(stream.disc(), 2)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -115,6 +117,13 @@ async def handle_shutdown(web_task):
|
|||||||
web_task.cancel()
|
web_task.cancel()
|
||||||
await web_task
|
await web_task
|
||||||
|
|
||||||
|
#
|
||||||
|
# now cancel all remaining (pending) tasks
|
||||||
|
#
|
||||||
|
pending = asyncio.all_tasks()
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
#
|
#
|
||||||
# at last, start a coro for stopping the loop
|
# at last, start a coro for stopping the loop
|
||||||
#
|
#
|
||||||
@@ -164,6 +173,7 @@ if __name__ == "__main__":
|
|||||||
logging.info(f'ConfigErr: {ConfigErr}')
|
logging.info(f'ConfigErr: {ConfigErr}')
|
||||||
Inverter.class_init()
|
Inverter.class_init()
|
||||||
Schedule.start()
|
Schedule.start()
|
||||||
|
mb_tcp = ModbusTcp(loop)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Create tasks for our listening servers. These must be tasks! If we call
|
# Create tasks for our listening servers. These must be tasks! If we call
|
||||||
|
|||||||
Reference in New Issue
Block a user