libs.zazac.link
Libs/capy/domo

capy.domo v0.5.4

Domotique : Philips Hue, WiZ, Shelly, Yeelight et Home Assistant, avec une seule façon de piloter

Inclus dans capy : installe capy pour l'avoir.

import capy.domo · PyPI : capyy · Python 3.9+ · aucune dépendance

Guide

La domotique avec capy : piloter les lampes et prises de la maison depuis Python ou le terminal, quelle que soit la marque, avec toujours les mêmes mots : on(), off(), brightness(30), color("orange"), white("chaud"). Sans dépendance, sans compte en ligne : tout se passe sur ton réseau local.

Les marques#

MarqueCe qu'il fautTrouvé tout seul
Philips Huele pont Hue, et appuyer une fois sur son boutonoui (SSDP, mDNS)
WiZ (Philips WiZ, Lidl, Trio…)rienoui
Shelly (prises, relais, variateurs, générations 1 et 2+)rienoui (mDNS)
Yeelight (Xiaomi)activer « Contrôle LAN » une fois dans l'appli Yeelightoui
Home Assistantun jeton d'accès, et c'est toutes ses marques d'un coup : IKEA, Sonoff, Tuya, Nanoleaf, Zigbee…oui (mDNS)

Ta marque n'est pas dans la liste ?

Si elle marche avec Home Assistant (presque toutes), branche Home Assistant : capy.domo pilote alors ses lampes et ses prises comme les autres. Et tu peux aussi ajouter une marque toi-même.

Démarrage#

  1. Sur le wifi de la maison, cherche les appareils : capy domo scan.
  2. Pour Hue : capy domo pair hue, puis appuie sur le bouton du pont quand on te le demande.
  3. Regarde ce qui a été trouvé : capy domo list.
  4. Pilote : capy domo salon on 30% orange, ou depuis Python.
terminal
capy domo scan
résultat
Recherche sur le réseau…
  + wiz-1234           wiz            192.168.1.31
  + yeelight-0123      yeelight       192.168.1.50
  + shellyplus1-abcd   shelly         192.168.1.40
  ! Pont Hue trouvé à 192.168.1.10 : appaire-le avec  capy domo pair hue 192.168.1.10
3 appareil(s) ajouté(s) dans /Users/izac/maison/capy.domo.json  (renomme-les dans ce fichier si tu veux)

Tout est rangé dans **capy.domo.json**, dans le dossier où tu lances la commande. Ouvre-le pour donner de vrais noms à tes appareils ("wiz-1234" → "bureau") : c'est ce nom qu'on utilise partout ensuite.

Seulement chez toi

Le scan trouve tout ce qui répond sur le wifi où tu es. Sur un wifi public ou chez quelqu'un d'autre, ne lance pas de scan : ce ne sont pas tes lampes.

Piloter depuis Python#

python
import capy.domo

home = capy.domo.Home()          # lit capy.domo.json

lampe = home["bureau"]
lampe.on()
lampe.brightness(30)             # en %, 0 = éteint
lampe.color("orange")            # un nom, "#FF7800" ou (255, 120, 0)
lampe.white("chaud")             # ou 2700 (kelvins) : chaud ≈ 2700, neutre ≈ 4000, froid ≈ 6000
lampe.set(brightness=80, color="bleu")   # plusieurs choses d'un coup
lampe.toggle()                   # allume si éteint, éteint si allumé
print(lampe.state())
résultat
{'on': False, 'brightness': 80, 'color': '#0000FF', 'kelvin': None, 'reachable': True}
MéthodeFaitPour
on() / off() / toggle()allume, éteint, inversetout
brightness(30)luminosité en % (0 = éteint)lampes
color("orange")couleur : nom, "#FF7800", (255, 120, 0)lampes couleur
white(2700)blanc, en kelvins ou "chaud", "neutre", "froid"lampes à blanc réglable
set(on=…, brightness=…, color=…, kelvin=…)plusieurs réglages d'un couplampes
state(){"on", "brightness", "color", "kelvin", "reachable"}tout

Régler une lampe éteinte l'allume

brightness(), color() et white() allument la lampe si elle était éteinte : pas besoin d'appeler on() avant.

Les couleurs

Les noms en français et en anglais marchent (rouge, orange, jaune, vert, cyan, bleu, violet, rose, blanc…). Si simplesuit est installé, ses 246 noms marchent aussi ("tomato", "mochapeach"…).

Une prise n'a pas de couleur

prise.brightness(50) lève ValueError: 'prise' est une prise / un interrupteur : seulement on, off et toggle. Pareil pour une lampe sans couleur à qui on demande color() : le message propose white() ou brightness().

Groupes et « tout »#

Un groupe se pilote exactement comme un appareil, et tous ses appareils changent en même temps (pas l'un après l'autre). home["tout"] est un groupe spécial avec tous les appareils.

python
home.set_group("salon", ["lampe-du-salon", "bureau", "prise-tv"])
home.save()

home["salon"].color("orange")    # la prise s'allume juste, les lampes passent en orange
home["tout"].off()               # tout éteindre avant de partir
print(home["salon"].state())     # {nom: état} pour chaque appareil

Chacun fait ce qu'il peut

Dans un groupe, une prise ignore la couleur et la luminosité (elle s'allume), une lampe sans couleur prend un blanc neutre. Si un appareil ne répond pas, les autres sont quand même réglés, puis une erreur dit lequel.

Scènes#

Une scène, c'est un réglage pour plusieurs appareils ou groupes, lancé d'un coup. Elles sont dans capy.domo.json :

capy.domo.json
"scenes": {
    "film": {
        "salon": {"brightness": 15, "color": "orange"},
        "cuisine": {"on": false}
    },
    "lecture": {
        "bureau": {"brightness": 100, "kelvin": 4000}
    }
}
python
home.scene("film")

home.save_scene("soiree")                  # Enregistre l'état actuel de tout comme une scène
home.save_scene("bureau", ["bureau"])      # … ou seulement de certains appareils
home.save()

Faire une scène sans écrire de JSON

Règle tes lampes comme tu veux (à la main, ou avec l'appli de la marque), puis home.save_scene("nom") et home.save() : la scène est copiée de ce que tu vois.

Le fichier capy.domo.json#

capy.domo.json
{
    "devices": {
        "salon": {"brand": "hue", "host": "192.168.1.10", "id": "1", "kind": "light", "color": true, "white": true},
        "bureau": {"brand": "wiz", "host": "192.168.1.31", "id": "a8bb50aa1234", "kind": "light"},
        "prise-tv": {"brand": "shelly", "host": "192.168.1.40", "id": "switch:0", "kind": "switch", "gen": 2},
        "cuisine": {"brand": "homeassistant", "id": "light.cuisine", "kind": "light"}
    },
    "groups": {"rdc": ["salon", "bureau", "prise-tv"]},
    "scenes": {"film": {"rdc": {"brightness": 15, "color": "orange"}}},
    "hue": {"192.168.1.10": "le-username-du-pont"},
    "homeassistant": {"url": "http://homeassistant.local:8123", "token": "$HA_TOKEN"}
}
  • Tu peux renommer un appareil en changeant sa clé ("wiz-1234" → "bureau"), et ses groupes avec.
  • capy.domo.Home() cherche le fichier à côté du script lancé, puis dans les dossiers au-dessus. Pour un autre : capy.domo.Home("~/maison/capy.domo.json").

Le username Hue et le jeton Home Assistant sont des mots de passe

Le fichier les contient : ne le mets pas sur GitHub. Pour Home Assistant, garde plutôt le jeton dans une variable d'environnement (export HA_TOKEN='…' dans ~/.zshrc) : pair homeassistant écrit alors seulement "$HA_TOKEN" dans le fichier. Ça marche pour toutes les valeurs : un texte qui commence par $ est lu dans les variables d'environnement.

Hue et Home Assistant : l'appairage#

Hue : le pont n'obéit qu'aux programmes qu'on a autorisés en appuyant sur son bouton.

terminal
capy domo pair hue
résultat
Appuie sur le gros bouton rond du pont Hue (192.168.1.10), puis sur Entrée…
  + lampe-du-salon     hue
  + lampe-chambre      hue
  + prise              hue
3 appareil(s) ajouté(s) dans /Users/izac/maison/capy.domo.json

Home Assistant : dans Home Assistant, ouvre ton profil → onglet Sécurité → Créer un jeton (jeton d'accès longue durée), puis :

terminal
capy domo pair homeassistant http://homeassistant.local:8123

Le jeton est demandé sans s'afficher (ou lu dans HA_TOKEN s'il existe). Toutes les lampes (light.…) et prises / interrupteurs (switch.…) de Home Assistant sont ajoutées.

À distance#

Une maison peut être pilotée depuis un autre ordinateur, le téléphone d'un ami… ou depuis n'importe où avec capy.network. Sur l'ordi qui reste à la maison (un Raspberry Pi, par exemple) :

terminal
capy domo serve

Il utilise le capy.config.json du dossier s'il y en a un (local, wifi ou network), sinon un serveur capy sur le port 4040. Puis, de l'autre côté, avec le même capy.config.json :

python
import capy.domo

maison = capy.domo.Remote()        # lit capy.config.json
maison["salon"].color("orange")    # mêmes méthodes qu'à la maison
maison.scene("film")
print(maison.list())               # appareils, groupes, scènes

Ou directement depuis le terminal : capy domo --remote salon off.

Allumer la maison depuis le bout du monde

Mets {"name": "maison", "relay": "mon-vps.fr:3879", "key": "$CAPY_KEY"} dans le capy.config.json des deux côtés : la maison se branche au relais, et ton client l'appelle par son nom, même derrière la box.

Qui peut appeler ta maison ?

Tous ceux qui peuvent joindre le serveur : tout le wifi en mode local ou wifi, et ceux qui ont la clé du relais en mode network. Ne partage pas cette clé.

La ligne de commande#

CommandeFait
capy domo scancherche les appareils et ajoute les nouveaux
capy domo pair hue [IP]appaire un pont Hue
capy domo pair homeassistant URLbranche Home Assistant
capy domo listappareils (avec leur état), groupes, scènes
capy domo salonl'état de salon
capy domo salon on 30% orangeallume, 30 %, orange (dans n'importe quel ordre)
capy domo tout offéteint tout
capy domo bureau chaud / 2700Kun blanc chaud
capy domo scene filmlance une scène
capy domo servepilotable à distance
capy domo --remote salon offpilote une maison à distance

Un raccourci dans ~/.zshrc

alias domo="capy domo" et c'est juste domo salon off.

Comment ça marche#

Chaque marque parle sa propre langue, et capy.domo traduit :

MarqueComment on la trouveComment on lui parle
HueSSDP + mDNS (_hue._tcp)HTTP vers le pont (API v1)
WiZmessage UDP à tout le wifi, port 38899UDP, petits messages JSON
ShellymDNS (_shelly._tcp, _http._tcp)HTTP (/rpc/… en gén. 2+, /relay/… en gén. 1)
YeelightSSDP sur le port 1982TCP, une ligne JSON par commande, port 55443
Home AssistantmDNS (_home-assistant._tcp)son API REST, avec le jeton

Le scan interroge toutes les marques en même temps, en 2 secondes environ. Chaque commande attend la réponse de l'appareil : state() renvoie "reachable": False si l'appareil est injoignable, et les autres méthodes lèvent ConnectionError: capy.domo : 'bureau' (wiz) ne répond pas : ….

Ajouter une marque#

Une marque, c'est une classe qui hérite de capy.domo.Device et écrit deux méthodes : _apply(changes) reçoit {"on", "brightness" (en %), "rgb" (r, g, b), "kelvin"} (seulement ce qui change), _state() renvoie l'état. Tout le reste (vérifications, couleurs, groupes, scènes, à distance, ligne de commande) vient tout seul.

ma_marque.py
import capy.domo


class MaLampe(capy.domo.Device):
    brand = "ma-marque"

    def _apply(self, changes):
        ...  # envoyer changes["on"], changes.get("brightness")… à l'appareil (self.host)

    def _state(self):
        return {"on": True, "brightness": 50}


capy.domo.BRANDS["ma-marque"] = MaLampe     # pour que capy.domo.json la reconnaisse

home = capy.domo.Home()
home.add(MaLampe("entree", "192.168.1.60"))
home.save()

Astuces et pièges#

Yeelight : « ne répond pas »

Le contrôle par le réseau est coupé par défaut : appli Yeelight → l'ampoule → réglages → Contrôle LAN.

WiZ ne descend pas sous 10 %

Les ampoules WiZ ont un minimum de 10 % : brightness(5) donne 10 %. brightness(0) éteint.

L'appareil a changé d'IP

Ta box peut donner une autre adresse à un appareil après une coupure. Relance capy domo scan, ou mieux : donne-lui une IP fixe dans les réglages de la box (« bail DHCP statique »).

Tester sans rien casser

state() ne change rien : parfait pour vérifier qu'un appareil répond. Et home["tout"].state() fait le tour de la maison.

Et Google Home, Alexa, Apple Maison ?

Ce sont des applis dans le cloud, pas des appareils : capy.domo parle directement aux appareils, sur ton réseau. Les lampes que tu pilotes avec Alexa sont souvent des Hue, WiZ ou Yeelight : capy.domo les pilote directement. Pour le reste, passe par Home Assistant.

Pourquoi l'API v1 de Hue ?

Elle marche sur tous les ponts, en HTTP simple, et suffit pour allumer, éteindre, régler la luminosité, la couleur et le blanc. capy.domo essaie d'abord en https, puis en http.

Exemples

capy.domo : trouver les appareils de la maison

Cherche sur le wifi les lampes et prises Hue, WiZ, Shelly, Yeelight et Home Assistant, en même temps. ⚠️ À lancer chez toi : sur un wifi public, ce ne sont pas tes lampes.

trouver_les_appareils.py
import capy.domo

home = capy.domo.Home()  # capy.domo.json à côté du script (créé au save())

nouveaux = home.scan()   # ~2 secondes
for appareil in nouveaux:
    print(f"+ {appareil.name:<18} {appareil.brand:<14} {appareil.host or appareil.id}")
for conseil in home.hints:  # Ex : « Pont Hue trouvé à 192.168.1.10 : appaire-le avec … »
    print("!", conseil)

home.save()
print(home)  # <capy.domo.Home 4 appareils, 0 groupes, 0 scènes (…/capy.domo.json)>

capy.domo : piloter une lampe

Les mêmes méthodes pour toutes les marques. Régler une lampe éteinte l'allume.

piloter_une_lampe.py
import time

import capy.domo

home = capy.domo.Home()
lampe = home["bureau"]  # Le nom donné dans capy.domo.json

lampe.on()
lampe.brightness(30)            # %
time.sleep(1)
lampe.color("orange")           # "orange", "#FF7800" ou (255, 120, 0)
time.sleep(1)
lampe.white("chaud")            # Ou 2700 (kelvins)
time.sleep(1)
lampe.set(brightness=100, color="bleu")

print(lampe.state())  # {'on': True, 'brightness': 100, 'color': '#0000FF', 'kelvin': None, 'reachable': True}
lampe.off()

capy.domo : groupes et scènes

Un groupe se pilote comme une lampe (tous ensemble, en même temps). Une scène règle plusieurs groupes d'un coup. Groupes et scènes sont gardés dans capy.domo.json.

groupes_et_scenes.py
import capy.domo

home = capy.domo.Home()

home.set_group("salon", ["lampe-du-salon", "bureau"])
home.scenes["film"] = {
    "salon": {"brightness": 15, "color": "orange"},
    "cuisine": {"on": False},
}
home.save()

home["salon"].white("chaud")   # Les deux lampes en même temps
home.scene("film")             # Tout le monde prend son réglage
home["tout"].off()             # « tout » : tous les appareils

home.save_scene("soiree")      # L'état actuel devient une scène
home.save()

capy.domo à distance : la maison devient un serveur capy

Sur l'ordi qui reste à la maison. Avec un capy.config.json, c'est son mode (local, wifi ou network) qui est utilisé : avec "relay", la maison est pilotable depuis tout internet. Sans, un serveur capy sur le port 4040.

maison.py
import capy.domo

capy.domo.serve()  # Actions domo.list, domo.state, domo.set, domo.toggle, domo.scene

capy.domo : un réveil lumineux

La lampe s'allume très doucement, d'un orange chaud vers un blanc de jour, comme un lever de soleil. Lance-le avec l'heure du réveil : python3 19_reveil_lumineux.py 7:30

reveil_lumineux.py
import datetime as dt
import sys
import time

import capy.domo

DUREE = 20 * 60  # Le lever de soleil dure 20 minutes
lampe = capy.domo.Home()["chambre"]

heure, minute = (int(x) for x in (sys.argv[1] if len(sys.argv) > 1 else "7:30").split(":"))
reveil = dt.datetime.now().replace(hour=heure, minute=minute, second=0, microsecond=0)
if reveil < dt.datetime.now():
    reveil += dt.timedelta(days=1)
debut = reveil - dt.timedelta(seconds=DUREE)
print(f"Lever de soleil de {debut:%H:%M} à {reveil:%H:%M} (Ctrl+C pour annuler)")
time.sleep(max((debut - dt.datetime.now()).total_seconds(), 0))

for etape in range(1, 61):  # 60 étapes : toutes les 20 secondes
    avance = etape / 60
    if avance < 0.4:
        lampe.set(brightness=1 + avance * 50, color=(255, round(80 + avance * 200), 0))  # Rouge-orangé qui monte
    else:
        lampe.set(brightness=avance * 100, kelvin=round(2200 + avance * 3800))         # Puis blanc de plus en plus froid
    time.sleep(DUREE / 60)
print("Debout ! ☀️")

Référence

Classes

class Device(name, host=None, id=None, kind='light', color=True, white=True, **extra)

Un appareil de la maison : lampe ("light") ou prise / interrupteur ("switch")

Toutes les marques ont les mêmes méthodes : on(), off(), toggle(), brightness(), color(), white(), state()

Device.config()

Ce qu'on garde dans capy.domo.json pour le retrouver

Voir le code
python
def config(self):
data = {"brand": self.brand, "host": self.host, "id": self.id, "kind": self.kind}
if self.kind == "light":
    data.update(color=self.has_color, white=self.has_white)
data.update(self.extra)
return {k: v for k, v in data.items() if v is not None}

Device.set(on=None, brightness=None, color=None, kelvin=None)

Changer plusieurs choses d'un coup (seulement ce qui est donné)

Exlampe.set(on=True, brightness=30, color="orange")

Voir le code
python
def set(self, on=None, brightness=None, color=None, kelvin=None):
changes = {}
if brightness is not None:
    changes["brightness"] = to_percent(brightness)
    if changes["brightness"] == 0:  # 0 % = éteint
        changes.pop("brightness")
        on = False
if color is not None:
    changes["rgb"] = to_rgb(color)
if kelvin is not None:
    changes["kelvin"] = to_kelvin(kelvin)
if self.kind == "switch" and changes:
    raise ValueError(f"capy.domo : {self.name!r} est une prise / un interrupteur : seulement on, off et toggle")
if "rgb" in changes and not self.has_color:
    raise ValueError(f"capy.domo : {self.name!r} ne fait pas de couleurs (essaie white() ou brightness())")
if "kelvin" in changes and not self.has_white:
    raise ValueError(f"capy.domo : {self.name!r} ne règle pas la température du blanc")
if "rgb" in changes and "kelvin" in changes:
    raise ValueError("capy.domo : une couleur OU un blanc, pas les deux à la fois")
if changes and on is None:
    on = True  # Régler la lumière d'une lampe éteinte l'allume
if on is not None:
    changes["on"] = bool(on)
if changes:
    try:
        self._apply(changes)
    except ConnectionError as e:
        raise ConnectionError(f"capy.domo : {self.name!r} ({self.brand}) ne répond pas : {e}") from None
return self

Device.on()

Pas de description.

Voir le code
python
def on(self):
return self.set(on=True)

Device.off()

Pas de description.

Voir le code
python
def off(self):
return self.set(on=False)

Device.toggle()

Allumer si éteint, éteindre si allumé

Voir le code
python
def toggle(self):
return self.set(on=not self.state()["on"])

Device.brightness(percent)

Luminosité en % (0 = éteint). Ex : lampe.brightness(30)

Voir le code
python
def brightness(self, percent):
return self.set(brightness=percent)

Device.color(color)

Couleur : un nom, "#FF7800" ou (255, 120, 0). Ex : lampe.color("orange")

Voir le code
python
def color(self, color):
return self.set(color=color)

Device.white(kelvin='neutre')

Blanc en kelvins : 2700 (chaud) … 6500 (froid), ou "chaud" / "neutre" / "froid"

Voir le code
python
def white(self, kelvin="neutre"):
return self.set(kelvin=kelvin)

Device.state()

État actuel -> {"on": True, "brightness": 50, "color": "#FF7800", "kelvin": None, "reachable": True}

Voir le code
python
def state(self):
try:
    state = self._state()
except ConnectionError:
    return {"on": False, "brightness": None, "color": None, "kelvin": None, "reachable": False}
return {"on": False, "brightness": None, "color": None, "kelvin": None, "reachable": True, **state}

class Group(name, devices)

Plusieurs appareils pilotés ensemble (en même temps, pas l'un après l'autre)

Exhome.group("salon").off() ; home.all().off()

Group.set(on=None, brightness=None, color=None, kelvin=None)

Dans un groupe, chacun fait ce qu'il peut : une prise s'allume juste,

une lampe sans couleur prend un blanc neutre quand on demande une couleur

Voir le code
python
def set(self, on=None, brightness=None, color=None, kelvin=None):
def one(device):
    if device.kind == "switch":
        wants = on
        if wants is None and any(v is not None for v in (brightness, color, kelvin)):
            wants = True
        if brightness is not None and to_percent(brightness) == 0:
            wants = False
        return device.set(on=wants)
    if color is not None and not device.has_color:
        return device.set(on=on, brightness=brightness, kelvin=kelvin if kelvin is not None else "neutre")
    return device.set(on=on, brightness=brightness, color=color, kelvin=kelvin)
self._run(one)
return self

Group.on()

Pas de description.

Voir le code
python
def on(self):
return self.set(on=True)

Group.off()

Pas de description.

Voir le code
python
def off(self):
return self.set(on=False)

Group.toggle()

Si au moins un est allumé : tout éteindre ; sinon tout allumer

Voir le code
python
def toggle(self):
return self.set(on=not any(s["on"] for s in self.state().values()))

Group.brightness(percent)

Pas de description.

Voir le code
python
def brightness(self, percent):
return self.set(brightness=percent)

Group.color(color)

Pas de description.

Voir le code
python
def color(self, color):
return self.set(color=color)

Group.state()

État de chaque appareil -> {nom: état}

Voir le code
python
def state(self):
return self._run(lambda device: device.state())

class Home(path=None)

Tous tes appareils, groupes et scènes, rangés dans capy.domo.json

Exhome = capy.domo.Home() ; home["salon"].on() ; home.scene("film")

Home.group(name)

Un groupe du fichier, ou "tout"

Voir le code
python
def group(self, name):
if name in EVERYTHING:
    return Group(name, self.devices.values())
if name not in self.groups:
    raise KeyError(f"capy.domo : aucun groupe {name!r} (groupes : {', '.join(self.groups) or 'aucun'})")
missing = [n for n in self.groups[name] if n not in self.devices]
if missing:
    raise KeyError(f"capy.domo : le groupe {name!r} contient {', '.join(missing)}, qui n'existe(nt) pas")
return Group(name, [self.devices[n] for n in self.groups[name]])

Home.all()

Tous les appareils

Voir le code
python
def all(self):
return self.group("tout")

Home.add(device, name=None)

Ajouter un appareil (nom choisi tout seul s'il est pris) -> son nom

Voir le code
python
def add(self, device, name=None):
base = slug(name or device.name)
name, n = base, 2
while name in self.devices or name in self.groups or name in EVERYTHING:
    name, n = f"{base}-{n}", n + 1
device.name = name
self.devices[name] = device
return name

Home.remove(name)

Pas de description.

Voir le code
python
def remove(self, name):
self.devices.pop(name)
for members in self.groups.values():
    if name in members:
        members.remove(name)

Home.set_group(name, members)

Créer (ou remplacer) un groupe. Ex : home.set_group("rdc", ["salon", "cuisine"])

Voir le code
python
def set_group(self, name, members):
unknown = [m for m in members if m not in self.devices]
if unknown:
    raise KeyError(f"capy.domo : appareil(s) inconnu(s) : {', '.join(unknown)}")
self.groups[slug(name)] = list(members)

Home.save()

Écrire capy.domo.json

Voir le code
python
def save(self):
data = {"devices": {name: d.config() for name, d in self.devices.items()},
        "groups": self.groups, "scenes": self.scenes}
if self.hue:
    data["hue"] = self.hue
if self.homeassistant:
    data["homeassistant"] = self.homeassistant
self.path.write_text(jsonlib.dumps(data, indent=4, ensure_ascii=False) + "\n", encoding="utf-8")
return self.path

Home.scene(name)

Lancer une scène : chaque appareil ou groupe prend son réglage, tous en même temps

Ex"film": {"salon": {"brightness": 20, "color": "orange"}, "cuisine": {"on": false}}

Voir le code
python
def scene(self, name):
if name not in self.scenes:
    raise KeyError(f"capy.domo : aucune scène {name!r} (scènes : {', '.join(self.scenes) or 'aucune'})")
settings = {target: {k: v for k, v in values.items() if k in ("on", "brightness", "color", "kelvin")}
            for target, values in self.scenes[name].items()}
Group(name, [self[target] for target in settings])._run(lambda target: target.set(**settings[target.name]))
return self

Home.save_scene(name, targets=None)

Enregistrer l'état actuel comme une scène. Ex : home.save_scene("soiree", ["salon", "cuisine"])

Voir le code
python
def save_scene(self, name, targets=None):
devices = [self.devices[t] for t in targets] if targets else list(self.devices.values())
scene = {}
for device_name, state in Group(name, devices).state().items():
    if not state["reachable"]:
        continue
    settings = {"on": state["on"]}
    if state["on"]:
        settings.update({k: state[k] for k in ("brightness", "color", "kelvin") if state[k] is not None})
        if "color" in settings and "kelvin" in settings:
            settings.pop("color")
    scene[device_name] = settings
self.scenes[slug(name)] = scene
return scene

Home.scan(timeout=2.0, brands=None, addresses=None, add=True)

Chercher les appareils du réseau -> [les nouveaux appareils] (pas encore enregistrés : save() pour les garder)

Les ponts Hue et Home Assistant pas encore appairés sont listés dans home.hints

addresses : pour viser une autre adresse que tout le réseau (tests), ex : {"wiz": ("127.0.0.1", 38899)}

Voir le code
python
def scan(self, timeout=2.0, brands=None, addresses=None, add=True):
brands, addresses = brands or list(BRANDS), addresses or {}
found, self.hints = [], []
jobs = {
    "wiz": lambda: _wiz.discover(timeout, **_address(addresses, "wiz")),
    "yeelight": lambda: _yeelight.discover(timeout, **_address(addresses, "yeelight")),
    "shelly": lambda: _shelly.discover(timeout, **_address(addresses, "shelly")),
    "hue": lambda: self._scan_hue(timeout, addresses),
    "homeassistant": lambda: self._scan_homeassistant(timeout, addresses),
}
results = {}

def run(brand):
    try:
        results[brand] = jobs[brand]()
    except (ConnectionError, OSError) as e:
        self.hints.append(f"{brand} : {e}")

threads = [threading.Thread(target=run, args=(b,)) for b in brands if b in jobs]
for t in threads:
    t.start()
for t in threads:
    t.join()
known = {(d.brand, d.host, d.id) for d in self.devices.values()}
for brand in brands:
    for device in results.get(brand, []):
        if (device.brand, device.host, device.id) not in known:
            known.add((device.brand, device.host, device.id))
            if add:
                self.add(device)
            found.append(device)
return found

Home.pair_hue(host)

Appairer un pont Hue (appuie sur son bouton juste avant) et ajouter ses lampes -> [lampes ajoutées]

Voir le code
python
def pair_hue(self, host):
self.hue[host] = _hue.pair(host)
return self._add_new(_hue.lights(host, self.hue[host]))

Home.pair_homeassistant(url, token)

Brancher Home Assistant et ajouter ses lampes et prises -> [appareils ajoutés]

token : le jeton, ou "$HA_TOKEN" pour le lire dans les variables d'environnement (conseillé)

Voir le code
python
def pair_homeassistant(self, url, token):
_homeassistant.check(url, _env(token))
self.homeassistant = {"url": url, "token": token}
return self._add_new(_homeassistant.entities(url, _env(token)))

class Remote(app=None)

Piloter une maison qui tourne ailleurs (avec serve()), avec les mêmes méthodes qu'en local

app : un capy.Project (par défaut capy.project(), lu dans capy.config.json)

Exmaison = capy.domo.Remote() ; maison["salon"].color("orange") ; maison.scene("film")

Remote.list()

Ce que la maison contient -> {"devices": {...}, "groups": {...}, "scenes": [...]}

Voir le code
python
def list(self):
return self._ask("domo.list")

Remote.state(name='tout')

Pas de description.

Voir le code
python
def state(self, name="tout"):
return self._ask("domo.state", name=name)

Remote.scene(name)

Pas de description.

Voir le code
python
def scene(self, name):
self._ask("domo.scene", name=name)
return self

class RemoteTarget(remote, name)

Un appareil ou un groupe d'une maison à distance (mêmes méthodes qu'un appareil local)

RemoteTarget.set(on=None, brightness=None, color=None, kelvin=None)

Pas de description.

Voir le code
python
def set(self, on=None, brightness=None, color=None, kelvin=None):
if isinstance(color, tuple):
    color = list(color)
self.remote._ask("domo.set", name=self.name, on=on, brightness=brightness, color=color, kelvin=kelvin)
return self

RemoteTarget.on()

Pas de description.

Voir le code
python
def on(self):
return self.set(on=True)

RemoteTarget.off()

Pas de description.

Voir le code
python
def off(self):
return self.set(on=False)

RemoteTarget.toggle()

Pas de description.

Voir le code
python
def toggle(self):
self.remote._ask("domo.toggle", name=self.name)
return self

RemoteTarget.state()

Pas de description.

Voir le code
python
def state(self):
states = self.remote.state(self.name)
return states.get(self.name, states) if len(states) == 1 else states

Fonctions

to_hex(rgb)

Pas de description.

Voir le code
python
def to_hex(rgb):
    return "#{:02X}{:02X}{:02X}".format(*rgb)

to_kelvin(value)

Un blanc -> kelvins. Accepte 2700, "2700K", "chaud", "neutre", "froid"

Voir le code
python
def to_kelvin(value):
    text = str(value).strip().lower()
    if text in WHITES:
        return WHITES[text]
    match = re.fullmatch(r"(\d+)\s*k?", text)
    if not match:
        raise ValueError(f"capy.domo : blanc inconnu {value!r} (un nombre de kelvins comme 2700, ou chaud / neutre / froid)")
    kelvin = int(match.group(1))
    if not 1000 <= kelvin <= 10000:
        raise ValueError(f"capy.domo : {kelvin} K n'est pas un blanc (de 2000 K, très chaud, à 6500 K, très froid)")
    return kelvin

to_percent(value)

Luminosité -> % entre 0 et 100. Accepte 50, 50.5, "50", "50%"

Voir le code
python
def to_percent(value):
    try:
        pct = float(str(value).strip().rstrip("%"))
    except ValueError:
        raise ValueError(f"capy.domo : luminosité {value!r} invalide (un pourcentage de 0 à 100)") from None
    if not 0 <= pct <= 100:
        raise ValueError(f"capy.domo : luminosité {pct:g} % hors de 0 à 100")
    return round(pct)

to_rgb(color)

Une couleur -> (r, g, b)

Accepte : "orange", "#FF7800", "#F70", "rgb(255, 120, 0)", (255, 120, 0), [255, 120, 0]

Voir le code
python
def to_rgb(color):
    if isinstance(color, (tuple, list)) and len(color) == 3:
        r, g, b = (int(c) for c in color)
        if all(0 <= c <= 255 for c in (r, g, b)):
            return r, g, b
        raise ValueError(f"capy.domo : chaque valeur RGB va de 0 à 255, reçu {tuple(color)}")
    text = str(color).strip().lower().replace(" ", "")
    if text in COLORS:
        return COLORS[text]
    match = re.fullmatch(r"rgb\((\d+),(\d+),(\d+)\)", text)
    if match:
        return to_rgb([int(x) for x in match.groups()])
    hexa = text.lstrip("#")
    if re.fullmatch(r"[0-9a-f]{3}", hexa):
        hexa = "".join(c * 2 for c in hexa)
    if re.fullmatch(r"[0-9a-f]{6}", hexa):
        return int(hexa[0:2], 16), int(hexa[2:4], 16), int(hexa[4:6], 16)
    try:  # Les 246 noms de simplesuit.colors, s'il est installé
        from simplesuit import colors
        return colors.to_rgb(text)
    except (ImportError, ValueError):
        pass
    close = difflib.get_close_matches(text, list(COLORS) + list(WHITES), n=3)
    hint = f" (tu voulais dire : {', '.join(close)} ?)" if close else " (un nom, \"#FF7800\" ou (255, 120, 0))"
    raise ValueError(f"capy.domo : couleur inconnue {color!r}{hint}")

slug(text)

"Lampe du Salon" -> "lampe-du-salon" (sans accents ni espaces : facile à taper)

Voir le code
python
def slug(text):
    text = unicodedata.normalize("NFD", str(text)).encode("ascii", "ignore").decode().lower()
    return re.sub(r"[^a-z0-9]+", "-", text).strip("-") or "appareil"

find(start=None)

capy.domo.json à côté du script, ou dans un dossier au-dessus (comme git)

Voir le code
python
def find(start=None):
    folder = pth.Path(start or _main_dir()).resolve()
    return next((place / FILE_NAME for place in (folder, *folder.parents) if (place / FILE_NAME).is_file()), None)

serve(home=None, server=None, run=True)

Rendre la maison pilotable à distance : des actions capy "domo.*" sur un serveur

Sans serveur : celui de capy.config.json s'il existe, sinon capy.Server(4040)

Excapy.domo.serve() puis, ailleurs : capy.domo.Remote()["salon"].on()

Voir le code
python
def serve(home=None, server=None, run=True):
    import capy
    home = home or Home()
    if server is None:
        server = capy.project().server() if capy._project.find() else capy.Server(4040)

    @server.action("domo.list")
    def domo_list():
        return {"devices": {n: {"brand": d.brand, "kind": d.kind, "color": d.has_color, "white": d.has_white}
                            for n, d in home.devices.items()},
                "groups": home.groups, "scenes": list(home.scenes)}

    @server.action("domo.state")
    def domo_state(name="tout"):
        target = home[name]
        return target.state() if isinstance(target, Group) else {name: target.state()}

    @server.action("domo.set")
    def domo_set(name, on=None, brightness=None, color=None, kelvin=None):
        home[name].set(on=on, brightness=brightness, color=color, kelvin=kelvin)
        return {"ok": True}

    @server.action("domo.toggle")
    def domo_toggle(name):
        home[name].toggle()
        return {"ok": True}

    @server.action("domo.scene")
    def domo_scene(name):
        home.scene(name)
        return {"ok": True}

    if run:
        server.run()
    return server

Constantes

COLORS

rouge(255, 0, 0)
red(255, 0, 0)
orange(255, 120, 0)
jaune(255, 210, 0)
yellow(255, 210, 0)
vert(0, 255, 0)
green(0, 255, 0)
cyan(0, 255, 255)
turquoise(64, 224, 208)
bleu(0, 0, 255)
blue(0, 0, 255)
violet(140, 0, 255)
purple(140, 0, 255)
magenta(255, 0, 255)
rose(255, 80, 160)
pink(255, 80, 160)
blanc(255, 255, 255)
white(255, 255, 255)

WHITES

chaud2700
warm2700
neutre4000
neutral4000
froid6000
cold6000
cool6000

EVERYTHING

EVERYTHING = ("tout", "all")  # home["tout"] : tous les appareils

BRANDS

Marque -> classe de l'appareil

hue<class 'capy.domo._hue.HueLight'>
wiz<class 'capy.domo._wiz.WizLight'>
shelly<class 'capy.domo._shelly.ShellyDevice'>
yeelight<class 'capy.domo._yeelight.YeelightBulb'>
homeassistant<class 'capy.domo._homeassistant.HassEntity'>