primo caricamento
This commit is contained in:
commit
d30984adfe
15 changed files with 658 additions and 0 deletions
49
README.md
Normal file
49
README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Add-on Domotica Work
|
||||
|
||||
Negozio di add-on Home Assistant per la flotta. Un add-on qui dentro si installa
|
||||
su qualunque impianto e si aggiorna su tutti alzando un numero di versione.
|
||||
|
||||
## Add-on disponibili
|
||||
|
||||
| Cartella | Nome | Cosa fa |
|
||||
|---|---|---|
|
||||
| `concentratore/` | Concentratore Modbus | Espone gli inverter ZCS al CCI e tiene rinfrescato il comando di modulazione |
|
||||
|
||||
## Registrare il repository su un impianto
|
||||
|
||||
Dall'interfaccia: **Impostazioni → Add-on → Negozio → ⋮ → Repository**, incollare
|
||||
l'URL del repository.
|
||||
|
||||
Oppure dal fleet, senza aprire nulla:
|
||||
|
||||
```
|
||||
ha_manage_addon(istanza="cliente-xxx", action="add_repository",
|
||||
repository="https://git.domotica.work/domotica/addons")
|
||||
ha_manage_addon(istanza="cliente-xxx", slug="<slug>", action="install")
|
||||
ha_manage_addon(istanza="cliente-xxx", slug="<slug>", options={...})
|
||||
ha_manage_addon(istanza="cliente-xxx", slug="<slug>", action="start")
|
||||
```
|
||||
|
||||
Il Supervisor clona **senza credenziali**: il repository deve essere leggibile
|
||||
in anonimo dagli impianti. Si chiude a livello di rete, non con un flag privato.
|
||||
|
||||
## Regole di casa
|
||||
|
||||
**Ogni modifica al codice alza `version` in `config.yaml`.** È l'unico
|
||||
meccanismo che fa comparire "Aggiorna" sugli impianti. Se si modifica il codice
|
||||
senza toccare la versione, gli impianti restano indietro e non lo segnala
|
||||
nessuno.
|
||||
|
||||
**Niente valori cablati nel codice.** IP, slave, porte, tempi: tutto in
|
||||
`options` con il relativo `schema`. È quello che permette allo stesso add-on di
|
||||
girare su venti impianti diversi senza toccarne il sorgente. Nel repository non
|
||||
deve finire niente di specifico di un cliente.
|
||||
|
||||
**Ogni add-on ha un `CHANGELOG.md`.** Home Assistant lo mostra nella schermata
|
||||
di aggiornamento: è lì che si legge cosa cambia prima di premere il pulsante.
|
||||
|
||||
## Aggiungere un add-on nuovo
|
||||
|
||||
Copiare `docs/modello/` in una cartella nuova alla radice del repository,
|
||||
scegliere uno `slug` univoco, riempire `config.yaml`. La cartella `docs/` non
|
||||
viene letta dal Supervisor, quindi il modello non compare nel negozio.
|
||||
24
concentratore/CHANGELOG.md
Normal file
24
concentratore/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Modifiche
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- **Corretta una corsa che poteva far perdere un comando.** La conferma
|
||||
registrata riguardava i valori presenti al ritorno della scrittura upstream,
|
||||
non quelli effettivamente inviati. Un setpoint che arrivava dal CCI durante
|
||||
quei ~300 ms veniva dichiarato confermato (registro 8192 a 1) senza essere
|
||||
mai stato spedito agli inverter.
|
||||
- **Il motore non muore piu' in silenzio.** Un'eccezione imprevista nel ciclo
|
||||
uccideva il thread; il concentratore continuava a rispondere al CCI con dati
|
||||
congelati e il watchdog non se ne accorgeva, perche' controlla solo che la
|
||||
porta 5020 sia aperta.
|
||||
- **La potenza di un inverter muto viene azzerata** dopo un periodo di grazia
|
||||
(5 volte il refresh, minimo 10 s), invece di restare congelata all'ultimo
|
||||
valore e continuare a sommarsi nel totale d'impianto (registro 8195).
|
||||
- **I comandi in attesa vengono propagati prima delle letture**, cosi' un
|
||||
setpoint nuovo non aspetta un giro di scansione completo.
|
||||
- Connessione upstream chiusa e riaperta dopo quattro risposte spaiate, per non
|
||||
restare disallineati sul flusso.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Prima versione confezionata come add-on.
|
||||
11
concentratore/Dockerfile
Normal file
11
concentratore/Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
|
||||
# Serve solo Python: il concentratore non usa librerie esterne
|
||||
RUN apk add --no-cache python3
|
||||
|
||||
COPY concentratore.py /concentratore.py
|
||||
COPY run.sh /run.sh
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
CMD [ "/run.sh" ]
|
||||
51
concentratore/README.md
Normal file
51
concentratore/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Concentratore Modbus
|
||||
|
||||
Si presenta al CCI come se fosse il gateway degli inverter: stessi slave ID,
|
||||
stessi registri. Risponde alle scritture in poche decine di millisecondi invece
|
||||
di ~310 ms, propaga il comando agli inverter veri e lo tiene rinfrescato, perche'
|
||||
i 50KTL-V3 lasciano decadere il limite se nessuno lo riscrive.
|
||||
|
||||
## Opzioni
|
||||
|
||||
| Opzione | Cosa e' |
|
||||
|---|---|
|
||||
| `gateway` | IP del Waveshare a cui sono appesi gli inverter |
|
||||
| `gateway_porta` | Porta Modbus TCP del Waveshare, di norma 502 |
|
||||
| `slaves` | Slave ID degli inverter, separati da virgola |
|
||||
| `refresh` | Ogni quanti secondi riscrivere il setpoint, in secondi |
|
||||
| `timeout` | Timeout delle transazioni verso il gateway |
|
||||
| `log` | Traccia a video ogni transazione |
|
||||
|
||||
Il CCI si collega alla **porta 5020** dell'impianto.
|
||||
|
||||
## Registri
|
||||
|
||||
Uguali a quelli degli inverter ZCS serie V3:
|
||||
|
||||
| Registro | Hex | Cosa | |
|
||||
|---|---|---|---|
|
||||
| 4357 | 0x1105 | abilitazione controllo remoto | R/W |
|
||||
| 4358 | 0x1106 | setpoint in decimi di percento | R/W |
|
||||
| 1157 | 0x0485 | potenza attiva, decine di W | R |
|
||||
|
||||
Aggiunti da noi, **da dichiarare ad AiLux**:
|
||||
|
||||
| Registro | Hex | Cosa |
|
||||
|---|---|---|
|
||||
| 8192 | 0x2000 | 1 = comando confermato su questo inverter |
|
||||
| 8193 | 0x2001 | decimi di secondo dall'ultima conferma (65535 = mai) |
|
||||
| 8194 | 0x2002 | quanti inverter hanno il comando confermato |
|
||||
| 8195 | 0x2003 | potenza totale d'impianto, decine di W |
|
||||
|
||||
## Prova
|
||||
|
||||
```
|
||||
mbpoll -a 1 -t 4 -p 5020 -r 4358 <ip-impianto> 1 200
|
||||
mbpoll -a 1 -t 4 -p 5020 -r 8192 -c 4 -1 <ip-impianto>
|
||||
```
|
||||
|
||||
## Attenzione
|
||||
|
||||
Fermando l'add-on gli inverter **restano con l'ultimo comando ricevuto**, e il
|
||||
limite decade da solo dopo qualche secondo. Non e' un modo per togliere il
|
||||
limite in sicurezza.
|
||||
3
concentratore/build.yaml
Normal file
3
concentratore/build.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-base:latest
|
||||
amd64: ghcr.io/home-assistant/amd64-base:latest
|
||||
409
concentratore/concentratore.py
Normal file
409
concentratore/concentratore.py
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CONCENTRATORE Modbus - si presenta al CCI come se fosse il gateway degli inverter.
|
||||
|
||||
Il CCI (o mbpoll, o Modula) si collega qui con gli STESSI slave ID e gli STESSI
|
||||
registri che userebbe sul Waveshare. Il concentratore:
|
||||
|
||||
- risponde SUBITO alle scritture (poche decine di ms invece di ~310 ms)
|
||||
- propaga il comando agli inverter veri e lo tiene RINFRESCATO, perche' i
|
||||
50KTL-V3 lasciano decadere il limite se nessuno lo riscrive
|
||||
- tiene in memoria le letture di potenza e le serve istantaneamente
|
||||
- dichiara in due registri di stato se il comando e' stato davvero recepito
|
||||
|
||||
Nessuna libreria da installare: solo la libreria standard di Python 3.
|
||||
|
||||
Avvio:
|
||||
python3 concentratore.py --gateway 192.168.4.240 --slaves 1,2,3,4,5
|
||||
|
||||
(porta 5020 di default: sotto la 1024 servirebbero i permessi di root)
|
||||
|
||||
Poi si prova puntandoci contro mbpoll, cambiando SOLO l'indirizzo:
|
||||
mbpoll -a 1 -t 4 -p 5020 -r 4359 -c 1 -1 127.0.0.1
|
||||
mbpoll -a 1 -t 4 -p 5020 -r 4358 127.0.0.1 1 200
|
||||
|
||||
REGISTRI ESPOSTI (identici a quelli degli inverter ZCS serie V3):
|
||||
4357 0x1105 abilitazione controllo remoto lettura/scrittura
|
||||
4358 0x1106 setpoint in decimi di percento lettura/scrittura
|
||||
1157 0x0485 potenza attiva (decine di W) sola lettura
|
||||
|
||||
REGISTRI DI STATO (aggiunti da noi - vanno dichiarati ad AiLux):
|
||||
8192 0x2000 1 = comando confermato su questo inverter, 0 = non confermato
|
||||
8193 0x2001 decimi di secondo dall'ultima conferma (65535 = mai)
|
||||
8194 0x2002 quanti inverter su N hanno il comando confermato
|
||||
8195 0x2003 potenza totale d'impianto in decine di W
|
||||
|
||||
NOTE DI VERSIONE
|
||||
1.0.2 - corretta la corsa fra CCI e motore: la conferma ora riguarda i
|
||||
valori davvero inviati, non quelli presenti al ritorno della
|
||||
scrittura. Prima un comando che arrivava durante la transazione
|
||||
upstream veniva dichiarato confermato senza essere mai spedito.
|
||||
- il thread motore non muore piu' in silenzio su un'eccezione
|
||||
imprevista: logga e riprende. Il watchdog dell'add-on controlla
|
||||
solo che la porta sia aperta, quindi non avrebbe visto nulla.
|
||||
- la potenza di un inverter che smette di rispondere viene azzerata
|
||||
dopo un periodo di grazia, invece di restare congelata per sempre
|
||||
e continuare a sommarsi nel registro 8195.
|
||||
- i comandi in attesa vengono propagati prima delle letture, cosi'
|
||||
un setpoint nuovo non aspetta un giro di scansione completo.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import socketserver
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
REG_ENABLE = 4357 # 0x1105
|
||||
REG_SETPOINT = 4358 # 0x1106
|
||||
REG_POWER = 1157 # 0x0485
|
||||
REG_ST_OK = 8192 # 0x2000
|
||||
REG_ST_ETA = 8193 # 0x2001
|
||||
REG_ST_CONTA = 8194 # 0x2002
|
||||
REG_ST_TOT = 8195 # 0x2003
|
||||
|
||||
cfg = {}
|
||||
stato = {}
|
||||
_stato_lock = threading.Lock()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ upstream
|
||||
|
||||
class Upstream:
|
||||
"""Connessione persistente verso il Waveshare. Una transazione alla volta."""
|
||||
|
||||
def __init__(self, ip, porta, timeout):
|
||||
self.ip, self.porta, self.timeout = ip, porta, timeout
|
||||
self.sock = None
|
||||
self.tid = 0
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def _apri(self):
|
||||
if self.sock:
|
||||
return
|
||||
self.sock = socket.create_connection((self.ip, self.porta), timeout=self.timeout)
|
||||
self.sock.settimeout(self.timeout)
|
||||
|
||||
def _chiudi(self):
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self.sock = None
|
||||
|
||||
def txn(self, unit, pdu):
|
||||
"""Restituisce il PDU di risposta, oppure None. Verifica id e slave."""
|
||||
with self.lock:
|
||||
for tentativo in range(2):
|
||||
try:
|
||||
self._apri()
|
||||
self.tid = (self.tid + 1) % 65000
|
||||
mio = self.tid
|
||||
self.sock.sendall(struct.pack('>HHHB', mio, 0, len(pdu) + 1, unit) + pdu)
|
||||
for _ in range(4):
|
||||
testa = self._leggi(6)
|
||||
t, proto, ln = struct.unpack('>HHH', testa)
|
||||
corpo = self._leggi(ln)
|
||||
if t == mio and corpo[0] == unit:
|
||||
return corpo[1:]
|
||||
# quattro risposte spaiate: il flusso e' disallineato,
|
||||
# meglio ripartire da una connessione pulita
|
||||
self._chiudi()
|
||||
return None
|
||||
except (OSError, struct.error):
|
||||
self._chiudi()
|
||||
if tentativo == 0:
|
||||
time.sleep(0.1)
|
||||
return None
|
||||
|
||||
def _leggi(self, n):
|
||||
buf = b''
|
||||
while len(buf) < n:
|
||||
c = self.sock.recv(n - len(buf))
|
||||
if not c:
|
||||
raise OSError('connessione chiusa')
|
||||
buf += c
|
||||
return buf
|
||||
|
||||
def leggi_reg(self, unit, addr, quanti=1):
|
||||
r = self.txn(unit, struct.pack('>BHH', 3, addr, quanti))
|
||||
if not r or r[0] != 3:
|
||||
return None
|
||||
n = r[1]
|
||||
if len(r) < 2 + n:
|
||||
return None
|
||||
return list(struct.unpack('>%dH' % quanti, r[2:2 + quanti * 2]))
|
||||
|
||||
def scrivi_reg(self, unit, addr, valori):
|
||||
dati = b''.join(struct.pack('>H', v & 0xFFFF) for v in valori)
|
||||
r = self.txn(unit, struct.pack('>BHHB', 16, addr, len(valori), len(dati)) + dati)
|
||||
return bool(r) and r[0] == 16
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ motore
|
||||
|
||||
def da_propagare(u):
|
||||
"""C'e' un comando da mandare a questo inverter, adesso?"""
|
||||
with _stato_lock:
|
||||
s = stato[u]
|
||||
if s['sporco']:
|
||||
return True
|
||||
# il rinfresco serve solo mentre il limite e' attivo: se il controllo
|
||||
# e' disabilitato basta scrivere una volta al cambiamento
|
||||
return bool(s['enable']) and (time.time() - s['ultimo_invio']) >= cfg['refresh']
|
||||
|
||||
|
||||
def propaga(up, u):
|
||||
"""Manda enable+setpoint a un inverter e registra cosa e' stato inviato."""
|
||||
with _stato_lock:
|
||||
s = stato[u]
|
||||
inviato = (s['enable'], s['setpoint'])
|
||||
|
||||
ok = up.scrivi_reg(u, REG_ENABLE, list(inviato))
|
||||
|
||||
with _stato_lock:
|
||||
s = stato[u]
|
||||
s['ultimo_invio'] = time.time()
|
||||
if ok:
|
||||
s['confermato'] = inviato
|
||||
s['ultima_conferma'] = time.time()
|
||||
# se nel frattempo il CCI ha scritto qualcosa di diverso, il lavoro
|
||||
# non e' finito: si resta sporchi e si riparte al giro successivo
|
||||
if (s['enable'], s['setpoint']) == inviato:
|
||||
s['sporco'] = False
|
||||
s['errori'] = 0
|
||||
else:
|
||||
s['errori'] += 1
|
||||
errori = s['errori']
|
||||
|
||||
if cfg['verboso']:
|
||||
print(' -> inv%d enable=%d sp=%d %s'
|
||||
% (u, inviato[0], inviato[1], 'ok' if ok else 'PERSA (x%d)' % errori))
|
||||
|
||||
|
||||
def aggiorna_potenza(up, u):
|
||||
"""Legge la potenza e la invecchia se l'inverter smette di rispondere."""
|
||||
regs = up.leggi_reg(u, REG_POWER, 1)
|
||||
with _stato_lock:
|
||||
s = stato[u]
|
||||
if regs:
|
||||
s['potenza'] = regs[0]
|
||||
s['letta'] = time.time()
|
||||
elif s['letta'] and (time.time() - s['letta']) > cfg['scadenza']:
|
||||
# meglio dichiarare zero che lasciare il CCI a regolare su una
|
||||
# potenza fantasma di un inverter che non c'e' piu'
|
||||
s['potenza'] = 0
|
||||
s['letta'] = None
|
||||
if cfg['verboso']:
|
||||
print(' !! inv%d muto da oltre %.0f s: potenza azzerata'
|
||||
% (u, cfg['scadenza']))
|
||||
|
||||
|
||||
def ciclo(up):
|
||||
inizio = time.time()
|
||||
|
||||
# prima i comandi in attesa, tutti: un setpoint nuovo non deve aspettare
|
||||
# che finisca il giro delle letture
|
||||
for u in cfg['slaves']:
|
||||
if da_propagare(u):
|
||||
propaga(up, u)
|
||||
|
||||
# poi le letture, ricontrollando fra una e l'altra se e' arrivato altro
|
||||
for u in cfg['slaves']:
|
||||
if da_propagare(u):
|
||||
propaga(up, u)
|
||||
aggiorna_potenza(up, u)
|
||||
|
||||
durata = time.time() - inizio
|
||||
if durata < 0.2:
|
||||
time.sleep(0.2 - durata)
|
||||
|
||||
|
||||
def motore(up):
|
||||
"""Propaga i comandi agli inverter e aggiorna le letture. Gira da solo.
|
||||
|
||||
Qualunque eccezione imprevista viene loggata e il ciclo riprende: se questo
|
||||
thread morisse, il concentratore continuerebbe a rispondere al CCI con dati
|
||||
congelati e nessuno se ne accorgerebbe (il watchdog guarda solo la porta).
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
ciclo(up)
|
||||
except Exception as e:
|
||||
print('MOTORE: eccezione imprevista, riprendo -> %r' % e)
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ registri
|
||||
|
||||
def leggi_virtuale(unit, addr, quanti):
|
||||
"""Serve le letture dalla memoria, senza toccare il bus."""
|
||||
fuori = []
|
||||
with _stato_lock:
|
||||
s = stato.get(unit)
|
||||
if not s:
|
||||
return None
|
||||
totale = sum(x['potenza'] for x in stato.values())
|
||||
confermati = sum(1 for x in stato.values()
|
||||
if x['confermato'] == (x['enable'], x['setpoint']))
|
||||
for i in range(quanti):
|
||||
a = addr + i
|
||||
if a == REG_ENABLE:
|
||||
fuori.append(s['enable'])
|
||||
elif a == REG_SETPOINT:
|
||||
fuori.append(s['setpoint'])
|
||||
elif a == REG_POWER:
|
||||
fuori.append(s['potenza'])
|
||||
elif a == REG_ST_OK:
|
||||
fuori.append(1 if s['confermato'] == (s['enable'], s['setpoint']) else 0)
|
||||
elif a == REG_ST_ETA:
|
||||
if not s['ultima_conferma']:
|
||||
fuori.append(65535)
|
||||
else:
|
||||
fuori.append(min(65534, int((time.time() - s['ultima_conferma']) * 10)))
|
||||
elif a == REG_ST_CONTA:
|
||||
fuori.append(confermati)
|
||||
elif a == REG_ST_TOT:
|
||||
fuori.append(min(65535, totale))
|
||||
else:
|
||||
return None
|
||||
return fuori
|
||||
|
||||
|
||||
def scrivi_virtuale(unit, addr, valori):
|
||||
"""Accetta il comando, risponde subito, lascia al motore il lavoro sporco."""
|
||||
with _stato_lock:
|
||||
s = stato.get(unit)
|
||||
if not s:
|
||||
return False
|
||||
for i, v in enumerate(valori):
|
||||
a = addr + i
|
||||
if a == REG_ENABLE:
|
||||
s['enable'] = v & 0xFFFF
|
||||
elif a == REG_SETPOINT:
|
||||
s['setpoint'] = max(0, min(1000, v & 0xFFFF))
|
||||
else:
|
||||
return False
|
||||
s['sporco'] = True
|
||||
return True
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ server
|
||||
|
||||
class Handler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
c = self.request
|
||||
c.settimeout(120)
|
||||
try:
|
||||
while True:
|
||||
testa = self._leggi(c, 6)
|
||||
if not testa:
|
||||
return
|
||||
tid, proto, ln = struct.unpack('>HHH', testa)
|
||||
corpo = self._leggi(c, ln)
|
||||
if not corpo or proto != 0:
|
||||
return
|
||||
unit, fc = corpo[0], corpo[1]
|
||||
t0 = time.time()
|
||||
pdu = self._servi(unit, fc, corpo[2:])
|
||||
c.sendall(struct.pack('>HHHB', tid, 0, len(pdu) + 1, unit) + pdu)
|
||||
if cfg['verboso']:
|
||||
print('CCI inv%d fc%-3d risposto in %5.1f ms'
|
||||
% (unit, fc, 1000 * (time.time() - t0)))
|
||||
except (OSError, struct.error):
|
||||
pass
|
||||
|
||||
def _leggi(self, c, n):
|
||||
buf = b''
|
||||
while len(buf) < n:
|
||||
try:
|
||||
d = c.recv(n - len(buf))
|
||||
except socket.timeout:
|
||||
return None
|
||||
if not d:
|
||||
return None
|
||||
buf += d
|
||||
return buf
|
||||
|
||||
def _servi(self, unit, fc, dati):
|
||||
try:
|
||||
if fc in (3, 4):
|
||||
addr, quanti = struct.unpack('>HH', dati[:4])
|
||||
v = leggi_virtuale(unit, addr, quanti)
|
||||
if v is None:
|
||||
return struct.pack('>BB', fc | 0x80, 2)
|
||||
return struct.pack('>BB', fc, quanti * 2) + b''.join(
|
||||
struct.pack('>H', x & 0xFFFF) for x in v)
|
||||
|
||||
if fc == 6:
|
||||
addr, val = struct.unpack('>HH', dati[:4])
|
||||
if not scrivi_virtuale(unit, addr, [val]):
|
||||
return struct.pack('>BB', 0x86, 2)
|
||||
return struct.pack('>BHH', 6, addr, val)
|
||||
|
||||
if fc == 16:
|
||||
addr, quanti = struct.unpack('>HH', dati[:4])
|
||||
nbyte = dati[4]
|
||||
valori = list(struct.unpack('>%dH' % quanti, dati[5:5 + nbyte]))
|
||||
if not scrivi_virtuale(unit, addr, valori):
|
||||
return struct.pack('>BB', 0x90, 2)
|
||||
return struct.pack('>BHH', 16, addr, quanti)
|
||||
except (struct.error, IndexError):
|
||||
return struct.pack('>BB', fc | 0x80, 3)
|
||||
return struct.pack('>BB', fc | 0x80, 1)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ avvio
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description='Concentratore Modbus per inverter ZCS')
|
||||
ap.add_argument('--gateway', default='192.168.4.240', help='IP del Waveshare')
|
||||
ap.add_argument('--gateway-porta', type=int, default=502)
|
||||
ap.add_argument('--slaves', default='1,2,3,4,5')
|
||||
ap.add_argument('--host', default='0.0.0.0', help='su quale interfaccia ascoltare')
|
||||
ap.add_argument('--porta', type=int, default=5020, help='porta di ascolto per il CCI')
|
||||
ap.add_argument('--refresh', type=float, default=2.0,
|
||||
help='ogni quanti secondi riscrivere il setpoint agli inverter')
|
||||
ap.add_argument('--timeout', type=float, default=1.5)
|
||||
ap.add_argument('--zitto', action='store_true', help='niente log a schermo')
|
||||
a = ap.parse_args()
|
||||
|
||||
slaves = [int(x) for x in a.slaves.split(',') if x.strip()]
|
||||
cfg.update(slaves=slaves,
|
||||
refresh=a.refresh,
|
||||
verboso=not a.zitto,
|
||||
# dopo quanto una lettura di potenza va considerata morta:
|
||||
# abbondante rispetto al giro di scansione, ma non infinita
|
||||
scadenza=max(10.0, 5 * a.refresh))
|
||||
|
||||
for u in cfg['slaves']:
|
||||
stato[u] = dict(enable=0, setpoint=1000, sporco=False,
|
||||
confermato=None, ultimo_invio=0.0, ultima_conferma=None,
|
||||
potenza=0, letta=None, errori=0)
|
||||
|
||||
up = Upstream(a.gateway, a.gateway_porta, a.timeout)
|
||||
threading.Thread(target=motore, args=(up,), daemon=True).start()
|
||||
|
||||
srv = Server((a.host, a.porta), Handler)
|
||||
print('CONCENTRATORE in ascolto su %s:%d' % (a.host, a.porta))
|
||||
print('inverter a valle: %s via %s:%d' % (cfg['slaves'], a.gateway, a.gateway_porta))
|
||||
print('rinfresco del setpoint ogni %.1f s (solo mentre il limite e\' attivo)' % a.refresh)
|
||||
print('potenza azzerata dopo %.0f s di silenzio di un inverter' % cfg['scadenza'])
|
||||
print('Ctrl+C per chiudere.\n')
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print('\nchiuso. Attenzione: gli inverter restano con l\'ultimo comando dato.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
30
concentratore/config.yaml
Normal file
30
concentratore/config.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Concentratore Modbus
|
||||
version: "1.0.2"
|
||||
slug: concentratore
|
||||
description: Espone gli inverter al CCI e mantiene rinfrescato il comando di modulazione
|
||||
url: https://www.diametra.it
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
init: false
|
||||
startup: services
|
||||
boot: auto
|
||||
watchdog: "tcp://[HOST]:[PORT:5020]"
|
||||
ports:
|
||||
5020/tcp: 5020
|
||||
ports_description:
|
||||
5020/tcp: Porta Modbus TCP a cui si collega il CCI
|
||||
options:
|
||||
gateway: 192.168.4.240
|
||||
gateway_porta: 502
|
||||
slaves: "1,2,3,4,5"
|
||||
refresh: 2.0
|
||||
timeout: 1.5
|
||||
log: true
|
||||
schema:
|
||||
gateway: str
|
||||
gateway_porta: port
|
||||
slaves: str
|
||||
refresh: float(0.5,30.0)
|
||||
timeout: float(0.2,10.0)
|
||||
log: bool
|
||||
25
concentratore/run.sh
Normal file
25
concentratore/run.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/with-contenv bashio
|
||||
# Legge le opzioni impostate dall'interfaccia dell'add-on e avvia il concentratore.
|
||||
|
||||
GATEWAY=$(bashio::config 'gateway')
|
||||
PORTA=$(bashio::config 'gateway_porta')
|
||||
SLAVES=$(bashio::config 'slaves')
|
||||
REFRESH=$(bashio::config 'refresh')
|
||||
TIMEOUT=$(bashio::config 'timeout')
|
||||
|
||||
ZITTO=""
|
||||
if ! bashio::config.true 'log'; then
|
||||
ZITTO="--zitto"
|
||||
fi
|
||||
|
||||
bashio::log.info "Gateway ${GATEWAY}:${PORTA} - inverter ${SLAVES} - rinfresco ${REFRESH}s"
|
||||
|
||||
exec python3 /concentratore.py \
|
||||
--gateway "${GATEWAY}" \
|
||||
--gateway-porta "${PORTA}" \
|
||||
--slaves "${SLAVES}" \
|
||||
--refresh "${REFRESH}" \
|
||||
--timeout "${TIMEOUT}" \
|
||||
--host 0.0.0.0 \
|
||||
--porta 5020 \
|
||||
${ZITTO}
|
||||
5
docs/modello/CHANGELOG.md
Normal file
5
docs/modello/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Modifiche
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Prima versione.
|
||||
10
docs/modello/Dockerfile
Normal file
10
docs/modello/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk add --no-cache python3
|
||||
|
||||
COPY programma.py /programma.py
|
||||
COPY run.sh /run.sh
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
CMD [ "/run.sh" ]
|
||||
7
docs/modello/README.md
Normal file
7
docs/modello/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Modello di add-on
|
||||
|
||||
Copiare questa cartella alla radice del repository, rinominarla, scegliere uno
|
||||
`slug` univoco e riempire `config.yaml`.
|
||||
|
||||
Promemoria: niente valori di un singolo cliente nel codice, tutto in `options`
|
||||
con il relativo `schema`, e ogni modifica successiva alza `version`.
|
||||
3
docs/modello/build.yaml
Normal file
3
docs/modello/build.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-base:latest
|
||||
amd64: ghcr.io/home-assistant/amd64-base:latest
|
||||
20
docs/modello/config.yaml
Normal file
20
docs/modello/config.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
name: Nome dell'add-on
|
||||
version: "1.0.0"
|
||||
slug: slug_univoco
|
||||
description: Una riga su cosa fa
|
||||
url: https://domotica.work
|
||||
arch:
|
||||
- aarch64
|
||||
- amd64
|
||||
init: false
|
||||
startup: services
|
||||
boot: auto
|
||||
# watchdog: "tcp://[HOST]:[PORT:0000]" # se l'add-on ascolta su una porta
|
||||
# ports:
|
||||
# 0000/tcp: 0000
|
||||
# ports_description:
|
||||
# 0000/tcp: A cosa serve questa porta
|
||||
options:
|
||||
esempio: valore
|
||||
schema:
|
||||
esempio: str
|
||||
8
docs/modello/run.sh
Normal file
8
docs/modello/run.sh
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/with-contenv bashio
|
||||
# Legge le opzioni impostate dall'interfaccia dell'add-on e avvia il programma.
|
||||
|
||||
ESEMPIO=$(bashio::config 'esempio')
|
||||
|
||||
bashio::log.info "Avvio con esempio=${ESEMPIO}"
|
||||
|
||||
exec python3 /programma.py --esempio "${ESEMPIO}"
|
||||
3
repository.yaml
Normal file
3
repository.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name: Add-on Domotica Work
|
||||
url: https://domotica.work
|
||||
maintainer: Domotica Work <info@domotica.work>
|
||||
Loading…
Add table
Add a link
Reference in a new issue