Metadata-Version: 2.5
Name: pyneosol
Version: 1.0.0
Summary: Python library for 868 MHz roller shutter USB dongles speaking the PFX AT serial protocol. Compatible with Profalux Neosol roller shutters and the MAI-DONGLE868-1A.
Project-URL: Homepage, https://github.com/bbayszczak/pyneosol
Project-URL: Repository, https://github.com/bbayszczak/pyneosol
Project-URL: Specification, https://github.com/bbayszczak/pyneosol/blob/main/docs/SPEC-PROTOCOLE-AT.md
Author: Benoit Bayszczak
License-Expression: MIT
License-File: LICENSE
Keywords: 868mhz,calypshome,home-automation,keeloq,neosol,profalux,rolling-shutter
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: pyserial-asyncio-fast>=0.16
Requires-Dist: pyserial>=3.5
Description-Content-Type: text/markdown

# pyneosol

[![CI](https://img.shields.io/github/actions/workflow/status/bbayszczak/pyneosol/ci.yml?branch=main&label=CI)](https://github.com/bbayszczak/pyneosol/actions/workflows/ci.yml)
[![Version](https://img.shields.io/github/v/release/bbayszczak/pyneosol?label=version)](https://github.com/bbayszczak/pyneosol/releases/latest)
[![Python](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/downloads/)
[![Licence](https://img.shields.io/badge/licence-MIT-green)](LICENSE)

Bibliothèque Python **asyncio** pour les dongles USB 868 MHz utilisant le protocole série AT
« PFX ». **Compatible avec** les volets roulants Profalux Neosol et le `MAI-DONGLE868-1A`.
Entièrement locale : ni box Calyps'HOME, ni cloud.

> ⚠️ **PROJET INDÉPENDANT, SANS AUCUNE AFFILIATION**
>
> `pyneosol` n'est **en aucun cas** affilié, soutenu, approuvé ou validé par Profalux, Stella
> Advanced Technology, ou l'une quelconque de leurs filiales, marques, sociétés apparentées,
> sous-traitants ou partenaires. *Profalux*, *Stella Advanced Technology*, *Neosol* et
> *Calyps'HOME* sont des marques de leurs titulaires respectifs, citées uniquement pour décrire
> le matériel avec lequel cette bibliothèque communique, à des fins d'interopérabilité.
>
> 👉 **Lisez impérativement l'avertissement complet ci-dessous avant toute utilisation.** Il
> couvre le **risque matériel** — certaines commandes du dongle sont destructives et peuvent
> effacer tous vos appairages —, l'**absence totale de garantie** et les conditions d'usage.
> En utilisant cette bibliothèque, vous reconnaissez les avoir lues et acceptées.

<details>
<summary><strong>⚠️ Avertissement complet — à lire avant toute utilisation</strong></summary>

**Ce projet est totalement indépendant et n'est en aucun cas affilié, soutenu, approuvé
ou validé par Profalux, Stella Advanced Technology, ou l'une quelconque de leurs
filiales, marques, sociétés apparentées, sous-traitants ou partenaires.**

*Profalux*, *Stella Advanced Technology*, *Neosol*, *NeosoL* et *Calyps'HOME* sont des
marques de leurs titulaires respectifs. Elles ne sont citées ici que pour **décrire le
matériel avec lequel cette bibliothèque est susceptible de communiquer**, à des fins
d'interopérabilité. Aucun code, aucun binaire, aucun micrologiciel, aucune clé
cryptographique et aucune documentation du fabricant n'est reproduit ni redistribué
dans ce dépôt.

**Nom du projet** — `pyneosol` est un nom d'usage choisi pour sa lisibilité. Il
n'emporte aucune affiliation, ne constitue ni une marque, ni une revendication
d'origine, ni une autorisation du titulaire de la marque *Neosol*. Cette bibliothèque
est un composant tiers **compatible avec** ce matériel, et rien d'autre.

**Méthode** — le protocole documenté ici a été reconstitué par la seule observation du
dialogue série avec un dongle acquis légalement, sur une installation appartenant à
l'auteur. Aucune décompilation de micrologiciel, aucune extraction ni publication de
clé cryptographique constructeur n'a été réalisée. Ce travail relève de l'exception
d'interopérabilité (art. L122-6-1 III et IV du Code de la propriété intellectuelle,
directive 2009/24/CE art. 5 et 6).

**Usage** — cette bibliothèque est destinée au pilotage d'équipements dont vous êtes
propriétaire ou légitime utilisateur, et à eux seuls.

**Absence de garantie** — ce logiciel est fourni « tel quel », sans aucune garantie
d'aucune sorte, expresse ou implicite, y compris, sans s'y limiter, les garanties de
qualité marchande, d'adéquation à un usage particulier et d'absence de contrefaçon.
Dans les limites permises par le droit applicable, l'auteur ne saurait être tenu
responsable d'un quelconque dommage : dysfonctionnement, détérioration de matériel,
perte de configuration ou d'appairage, perte de données, ou tout dommage direct ou
indirect résultant de l'utilisation de cette bibliothèque.

**Risque matériel** — certaines commandes du dongle sont **destructives** et peuvent
effacer ses appairages (voir la section *Commandes dangereuses* des
[spécifications](docs/SPEC-PROTOCOLE-AT.md)). Sauvegardez la configuration de votre
dongle avant toute expérimentation.

**Garantie constructeur** — l'usage de ce logiciel avec votre matériel est susceptible
d'en affecter la garantie. Vérifiez-le avant de l'utiliser.

**Support** — assuré bénévolement, sans engagement de délai ni de résultat.

**Licence** — MIT, voir [LICENSE](LICENSE).

En utilisant cette bibliothèque, vous reconnaissez avoir lu et accepté l'ensemble de
ces conditions.

</details>

---

## Sommaire

- [Démarrage rapide](#démarrage-rapide)
- [Matériel](#matériel)
- [État du projet](#état-du-projet)
- [Installation](#installation)
- [Utilisation](#utilisation)
- [Logging](#logging)
- [Développement](#développement)
- [Contribuer](#contribuer)
- [Sécurité](#sécurité)

---

## Démarrage rapide

Avec [`uv`](https://docs.astral.sh/uv/) et le dongle branché :

```bash
git clone https://github.com/bbayszczak/pyneosol
cd pyneosol
uv run demo.py
```

`uv` crée l'environnement et installe les dépendances tout seul. Sans argument, `demo.py`
**n'émet rien** : il identifie le dongle et liste les canaux utilisés — de quoi vérifier en
quelques secondes que votre matériel est reconnu.

---

## Matériel

Cette bibliothèque est **compatible avec** le dongle USB 868 MHz distribué pour les volets
roulants Profalux Neosol. Elle n'est ni fournie, ni distribuée, ni approuvée par le fabricant.

Elle **fonctionne avec le dongle photographié ci-dessous**, le `MAI-DONGLE868-1A`, sur lequel
tout le protocole a été validé.

<!-- Photos du dongle de l'auteur : métadonnées supprimées et numéro de série masqué à la
     source. Toute image ajoutée ici doit conserver ces deux propriétés. -->
<p align="center">
  <img src="docs/images/dongle-mai-dongle868-1a-face.jpg"
       alt="Dongle USB Neosol vu de face" width="190">
  &nbsp;&nbsp;&nbsp;
  <img src="docs/images/dongle-mai-dongle868-1a-etiquette.jpg"
       alt="Étiquette du dongle : Dongle Neosol, MAI-DONGLE868-1A, Lot 25/22, 5V 0.15 W"
       width="190">
  <br>
  <em>Le dongle validé, de face et côté étiquette. Le numéro de série est masqué.</em>
</p>

| | |
|---|---|
| **Modèle validé** | `MAI-DONGLE868-1A` |
| **Identification interne** | `PFX KEELOQ` |
| **Hardware Version** | `0` |
| **Software Version** | `Rev10` |
| **Interface** | USB CDC-ACM (port série virtuel) |
| **Débit** | 115200 bauds |

> ℹ️ **`MAI-DONGLE868-1A` est la seule référence sur laquelle le protocole a été validé**, et
> la seule dont dispose l'auteur. L'existence et le comportement d'éventuelles autres
> références ne sont pas connus : rien n'est vérifié ni garanti sur une autre référence, une
> autre version matérielle ou une autre révision logicielle. Les retours sur d'autres modèles
> sont les bienvenus.

### Identifier votre dongle

Avant d'ouvrir une issue, indiquez toujours **la référence de votre dongle ainsi que ses
versions matérielle et logicielle**. Elles s'obtiennent avec la commande AT `AT&V`.

**1. Repérer le port série**

```bash
# Linux
ls -l /dev/ttyACM*

# macOS
ls -1 /dev/cu.usbmodem*
```

**2. Interroger le dongle**

```bash
python3 -c "
import serial, time
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)   # adapter le port
time.sleep(0.4)
ser.write(b'AT&V\r\n'); ser.flush()
time.sleep(1.5)
print(ser.read(ser.in_waiting).decode('utf-8', 'replace'))
ser.close()
"
```

**3. Sortie attendue**

```
PFX KEELOQ

Hardware Version:  0

Software Version: Rev10

S/N: XXXXXXXX

ACTIVE CONFIG :

Return Code Active : 1

Frame Repeat Nb : T0=25,T1=15,T2=70,T3=70

Read Protection Active : 0

AT&V:OK
```

La ligne `PFX KEELOQ` confirme qu'il s'agit bien d'un dongle de cette famille. `Hardware
Version` et `Software Version` sont les deux valeurs à communiquer en cas de problème.

> 🔐 Le numéro de série (`S/N`) identifie votre exemplaire : inutile de le publier.

---

## État du projet

**Stable.** Le pilotage fonctionne, le protocole est documenté dans
[`docs/SPEC-PROTOCOLE-AT.md`](docs/SPEC-PROTOCOLE-AT.md), et l'API publique suit le
versionnage sémantique : tout changement cassant passe par une version majeure.

Les changements de chaque version sont consignés dans le [CHANGELOG](CHANGELOG.md), tenu à jour
automatiquement à partir des messages de commit.

---

## Installation

Le paquet est publié sur [PyPI](https://pypi.org/project/pyneosol/) :

```bash
pip install pyneosol
```

Avec [`uv`](https://docs.astral.sh/uv/) :

```bash
uv add pyneosol
```

## Utilisation

La bibliothèque est **asynchrone** : tout échange avec le dongle est une coroutine, et rien
n'y bloque la boucle d'événements — ni l'attente des réponses, ni l'ouverture du port, ni la
détection.

```python
import asyncio

from pyneosol import Dongle


async def main() -> None:
    async with Dongle.connect() as dongle:  # détection automatique du port
        print((await dongle.info()).software_version)

        for channel in await dongle.used_channels():
            print(channel)  # la clé n'est jamais affichée

        await dongle.close_shutter(0)  # descente
        await dongle.stop(0)  # arrêt en cours de course
        await dongle.favourite(0)  # position favorite


asyncio.run(main())
```

Le port peut aussi être imposé : `Dongle.connect("/dev/ttyACM0")`.

### Ouvrir sans bloc `async with`

`Dongle.connect()` est un gestionnaire de contexte asynchrone : il ouvre le dongle et le
referme en sortant du bloc. Quand la durée de vie du dongle dépasse un bloc — le cas d'une
intégration domotique, qui l'ouvre au démarrage et le referme à l'arrêt —, utilisez
`Dongle.open()`, qui rend simplement le dongle ouvert :

```python
dongle = await Dongle.open("/dev/ttyACM0")
try:
    await dongle.open_shutter(0)
finally:
    await dongle.close()
```

`Dongle.connect()` n'existe que pour éviter la forme `async with await Dongle.open()` ; les
deux prennent les mêmes arguments.

### Détection du port

`find_ports()` est également une coroutine. L'énumération des ports parcourt l'arborescence
des périphériques de l'hôte — `/sys` sous Linux, IOKit sous macOS —, ce qui bloque : elle est
donc exécutée dans un thread de travail plutôt que sur la boucle. `Dongle.open()` l'appelle
lorsqu'aucun port n'est précisé, autrement dit elle se trouve sur le chemin asynchrone : une
bibliothèque asyncio n'a pas à y glisser d'appel bloquant à l'insu de l'appelant.

```python
for port in await find_ports():
    print(port.device, port.manufacturer, port.product)
```

> ⚠️ **Aucun retour d'état.** Le dongle ne fait qu'émettre. Une commande acceptée signifie
> qu'une trame est partie, jamais qu'un volet a bougé, et aucune position n'est lisible.
> Toute notion d'état ou de position relève de la couche appelante.

### Essayer sans écrire de code

Le dépôt fournit un script de démonstration à sa racine :

```bash
uv run demo.py                           # identification et table des canaux
uv run demo.py --port /dev/ttyACM0       # forcer le port série
uv run demo.py --channel 2 --close       # descente, avec confirmation
uv run demo.py --channel 2 --stop --yes  # stop, sans confirmation
uv run demo.py --debug                   # afficher le dialogue AT (voir Logging)
```

Sans argument, il **n'émet rien** : il se contente d'identifier le dongle et de lister les
canaux utilisés. C'est le moyen le plus rapide de vérifier que votre dongle est reconnu.

Les actions (`--open`, `--close`, `--stop`, `--favourite`) doivent être demandées explicitement
et déclenchent une confirmation, puisqu'elles déplacent un volet réel. Les clés ne sont jamais
affichées.

Exemple de sortie (valeurs factices) :

```
Looking for a dongle...
  /dev/ttyACM0  PROFALUX / KEELOQ USB Device

Dongle
  hardware version : 0
  software version : Rev10
  frame repeat     : T0=25,T1=15,T2=70,T3=70
  read protection  : no
  transmit power   : 14

Channels: 50 total, 2 used
  channel  0  serial 000AAAA1  sync 43
  channel  1  serial 000AAAA2  sync 14

Read-only run: nothing was transmitted.
```

## Logging

La bibliothèque utilise le module `logging` standard et **ne configure rien** : ni handler, ni
niveau, ni format. C'est l'application hôte qui décide. Chaque module a son logger, nommé
d'après le paquet — `pyneosol.dongle`, `pyneosol.discovery` — ce qui permet de filtrer au
paquet entier comme au module.

Tout est en `DEBUG` : le dialogue AT (commande émise, réponse, durée) et la détection du port.
Les erreurs ne sont pas loguées, elles sont **levées** ; c'est à l'appelant de décider ce qu'il
en fait.

Dans Home Assistant, via `configuration.yaml` :

```yaml
logger:
  logs:
    pyneosol: debug          # tout
    pyneosol.dongle: debug   # seulement le dialogue AT
```

En script autonome :

```python
import logging

logging.basicConfig(level=logging.DEBUG)
```

Exemple de trace (valeurs factices) :

```
DEBUG pyneosol.discovery: 1 of 4 serial ports match 10C4:0003: ['/dev/ttyACM0']
DEBUG pyneosol.dongle: opening /dev/ttyACM0
DEBUG pyneosol.dongle: > AT$C?
DEBUG pyneosol.dongle: < ['0,***,0029, ***', '1,***,0009, ***', 'AT$C:OK'] (0.212s)
DEBUG pyneosol.dongle: > AT$SF=0,1
DEBUG pyneosol.dongle: < ['AT$SF:OK'] (0.004s)
```

> ✅ **Aucun secret n'entre dans les logs.** Le masquage joue **dans les deux sens** : les
> réponses du dongle comme les commandes envoyées. Les clés KeeLoq et les numéros de série
> deviennent `***`, tandis que l'index du canal et son compteur `sync` restent lisibles. Une
> trace `DEBUG` peut donc être jointe telle quelle à un rapport de bug.
>
> Le sens commande compte parce que `Dongle.execute()` accepte des commandes brutes : une
> `AT$C=` ou une `AT$SN=` formée à la main porte son secret dans la commande elle-même. Elle
> est loguée `AT$C=0,***,0029,***`. Les messages d'exception suivent la même règle.
>
> **Le chemin du port** est masqué lui aussi, car il nomme votre exemplaire sur certains
> hôtes : macOS baptise le nœud d'après le numéro de série USB
> (`/dev/cu.usbmodem0000000012341`), tout comme les liens `by-id` sous Linux. Seules les
> suites d'au moins quatre chiffres disparaissent — `/dev/ttyACM0` reste lisible tel quel :
>
> ```
> DEBUG pyneosol.discovery: 1 of 4 serial ports match 10C4:0003: ['/dev/cu.usbmodem***']
> DEBUG pyneosol.dongle: opening /dev/cu.usbmodem***
> ```

## Développement

```bash
uv run ruff check .      # lint
uv run ruff format .     # formatage
uv run pytest            # tests — aucun matériel requis, le dongle est simulé
```

---

## Contribuer

Les retours sont bienvenus, en particulier sur **d'autres références de dongle** que le
`MAI-DONGLE868-1A`, seule référence validée à ce jour.

- **Signaler un bug ou proposer une évolution** —
  [ouvrir une issue](https://github.com/bbayszczak/pyneosol/issues/new/choose), en précisant la
  référence de votre dongle et ses versions matérielle et logicielle
  (voir [Identifier votre dongle](#identifier-votre-dongle)).
- **Proposer du code** — périmètre, conventions et interdits sont décrits dans
  [CONTRIBUTING.md](CONTRIBUTING.md), à lire avant d'ouvrir une pull request.

---

## Sécurité

Le dongle stocke les **clés KeeLoq** de vos volets, lisibles en clair via la commande
`AT$C?`. Quiconque les possède peut commander vos volets.

Pour signaler une faille, utilisez le [signalement privé](SECURITY.md) — jamais une issue
publique.

**Ne publiez jamais le contenu réel de votre table de canaux** — ni dans une issue, ni dans un
rapport de bug, ni dans un export de configuration. Masquez systématiquement les clés et les
numéros de série avant tout partage.
