commit d30984adfe904a313908b22ff4b6d0f7161b0e24 Author: andrea castagnino Date: Thu Aug 27 12:28:06 2026 +0200 primo caricamento diff --git a/README.md b/README.md new file mode 100644 index 0000000..58dfcd0 --- /dev/null +++ b/README.md @@ -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="", action="install") +ha_manage_addon(istanza="cliente-xxx", slug="", options={...}) +ha_manage_addon(istanza="cliente-xxx", 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. diff --git a/concentratore/CHANGELOG.md b/concentratore/CHANGELOG.md new file mode 100644 index 0000000..80567bc --- /dev/null +++ b/concentratore/CHANGELOG.md @@ -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. diff --git a/concentratore/Dockerfile b/concentratore/Dockerfile new file mode 100644 index 0000000..1b93d9d --- /dev/null +++ b/concentratore/Dockerfile @@ -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" ] diff --git a/concentratore/README.md b/concentratore/README.md new file mode 100644 index 0000000..81ab1c8 --- /dev/null +++ b/concentratore/README.md @@ -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 1 200 +mbpoll -a 1 -t 4 -p 5020 -r 8192 -c 4 -1 +``` + +## 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. diff --git a/concentratore/build.yaml b/concentratore/build.yaml new file mode 100644 index 0000000..3e9c240 --- /dev/null +++ b/concentratore/build.yaml @@ -0,0 +1,3 @@ +build_from: + aarch64: ghcr.io/home-assistant/aarch64-base:latest + amd64: ghcr.io/home-assistant/amd64-base:latest diff --git a/concentratore/concentratore.py b/concentratore/concentratore.py new file mode 100644 index 0000000..f5ddb88 --- /dev/null +++ b/concentratore/concentratore.py @@ -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() diff --git a/concentratore/config.yaml b/concentratore/config.yaml new file mode 100644 index 0000000..5790060 --- /dev/null +++ b/concentratore/config.yaml @@ -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 diff --git a/concentratore/run.sh b/concentratore/run.sh new file mode 100644 index 0000000..1f3ca82 --- /dev/null +++ b/concentratore/run.sh @@ -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} diff --git a/docs/modello/CHANGELOG.md b/docs/modello/CHANGELOG.md new file mode 100644 index 0000000..fc9deeb --- /dev/null +++ b/docs/modello/CHANGELOG.md @@ -0,0 +1,5 @@ +# Modifiche + +## 1.0.0 + +- Prima versione. diff --git a/docs/modello/Dockerfile b/docs/modello/Dockerfile new file mode 100644 index 0000000..449aa98 --- /dev/null +++ b/docs/modello/Dockerfile @@ -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" ] diff --git a/docs/modello/README.md b/docs/modello/README.md new file mode 100644 index 0000000..95514ab --- /dev/null +++ b/docs/modello/README.md @@ -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`. diff --git a/docs/modello/build.yaml b/docs/modello/build.yaml new file mode 100644 index 0000000..3e9c240 --- /dev/null +++ b/docs/modello/build.yaml @@ -0,0 +1,3 @@ +build_from: + aarch64: ghcr.io/home-assistant/aarch64-base:latest + amd64: ghcr.io/home-assistant/amd64-base:latest diff --git a/docs/modello/config.yaml b/docs/modello/config.yaml new file mode 100644 index 0000000..5e68f96 --- /dev/null +++ b/docs/modello/config.yaml @@ -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 diff --git a/docs/modello/run.sh b/docs/modello/run.sh new file mode 100644 index 0000000..b4113e3 --- /dev/null +++ b/docs/modello/run.sh @@ -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}" diff --git a/repository.yaml b/repository.yaml new file mode 100644 index 0000000..3abdac4 --- /dev/null +++ b/repository.yaml @@ -0,0 +1,3 @@ +name: Add-on Domotica Work +url: https://domotica.work +maintainer: Domotica Work