pyhaseiq 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ name: Rapport de bug
2
+ description: Un comportement inattendu de la bibliothèque ou du poêle
3
+ labels: [bug]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ ⚠️ **Deux générations de poêles portent le nom « Hase iQ ».** Cette bibliothèque ne
9
+ parle qu'à **l'ancienne**, celle pilotée par l'application `flamemonitor`. Si votre
10
+ poêle se pilote avec l'application `HASE iQ`, elle ne fonctionnera pas — ce n'est pas
11
+ un bug.
12
+ - type: dropdown
13
+ id: generation
14
+ attributes:
15
+ label: Application de pilotage
16
+ description: Celle que vous utilisez habituellement avec votre poêle.
17
+ options:
18
+ - flamemonitor (ancienne génération — supportée)
19
+ - Hase iQ (nouvelle génération — non supportée)
20
+ - Je ne sais pas
21
+ validations:
22
+ required: true
23
+ - type: textarea
24
+ id: description
25
+ attributes:
26
+ label: Description
27
+ description: Ce qui se passe, et ce que vous attendiez.
28
+ validations:
29
+ required: true
30
+ - type: textarea
31
+ id: reproduction
32
+ attributes:
33
+ label: Reproduction
34
+ description: Les étapes, et le code minimal qui déclenche le problème.
35
+ validations:
36
+ required: true
37
+ - type: input
38
+ id: versions
39
+ attributes:
40
+ label: Versions du poêle
41
+ description: >-
42
+ Sortie de `uv run demo.py <ip> --request _oemver --request _wversion --request _oemdev`.
43
+ placeholder: _oemver = AAF_5815=9, _wversion = 1.4, _oemdev = 2
44
+ validations:
45
+ required: true
46
+ - type: input
47
+ id: environment
48
+ attributes:
49
+ label: Environnement
50
+ placeholder: macOS 15.4, Python 3.13, pyhaseiq 0.1.0
51
+ validations:
52
+ required: true
53
+ - type: textarea
54
+ id: logs
55
+ attributes:
56
+ label: Logs
57
+ description: Le dialogue WebSocket si vous l'avez (`demo.py --debug`). Rendu en bloc de code.
58
+ render: text
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Faille de sécurité
4
+ url: https://github.com/bbayszczak/pyhaseiq/security/advisories/new
5
+ about: Signalement privé — n'ouvrez jamais d'issue publique pour une vulnérabilité.
@@ -0,0 +1,29 @@
1
+ name: Proposition
2
+ description: Une évolution de la bibliothèque
3
+ labels: [enhancement]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ `pyhaseiq` est une bibliothèque **en lecture seule** et **sans état** : elle interroge
9
+ le poêle, rien d'autre. Historisation, seuils, notifications et toute écriture
10
+ appartiennent à la couche appelante. Voir [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md).
11
+ - type: textarea
12
+ id: besoin
13
+ attributes:
14
+ label: Besoin
15
+ description: Le problème concret que cela résout, pas la solution envisagée.
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: proposition
20
+ attributes:
21
+ label: Proposition
22
+ description: Ce que vous imaginez, et l'état de validation du protocole s'il est concerné.
23
+ - type: checkboxes
24
+ id: perimetre
25
+ attributes:
26
+ label: Périmètre
27
+ options:
28
+ - label: Cette proposition n'écrit rien sur le poêle et ne conserve aucun état dans la bibliothèque.
29
+ required: true
@@ -0,0 +1,12 @@
1
+ # Les actions sont épinglées sur des SHA complets dans .github/workflows (protection contre
2
+ # le redéplacement d'un tag). Dependabot est donc indispensable : sans lui, ces SHA figés ne
3
+ # recevraient jamais les correctifs de sécurité amont.
4
+ version: 2
5
+ updates:
6
+ - package-ecosystem: github-actions
7
+ directory: /
8
+ schedule:
9
+ interval: weekly
10
+ commit-message:
11
+ # Conventional Commits : `ci` n'entraîne pas de release (cf. release-please).
12
+ prefix: ci
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ lint:
17
+ name: Lint
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
22
+ with:
23
+ enable-cache: true
24
+ - name: ruff check
25
+ run: uv run ruff check --output-format=github .
26
+ - name: ruff format
27
+ run: uv run ruff format --check --diff .
28
+
29
+ test:
30
+ name: Test (Python ${{ matrix.python-version }})
31
+ runs-on: ubuntu-latest
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ python-version: ["3.13", "3.14"]
36
+ steps:
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
39
+ with:
40
+ enable-cache: true
41
+ python-version: ${{ matrix.python-version }}
42
+ # The test suite runs against a fake stove, so no hardware is required.
43
+ - name: pytest
44
+ run: uv run --python ${{ matrix.python-version }} pytest
@@ -0,0 +1,141 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ concurrency:
11
+ group: release-${{ github.ref }}
12
+ cancel-in-progress: false
13
+
14
+ jobs:
15
+ # Maintient une PR de release à partir des Conventional Commits : elle accumule le
16
+ # CHANGELOG et le bump de version, et déclenche tag + GitHub Release une fois fusionnée.
17
+ release-please:
18
+ name: Release PR
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ # Le GITHUB_TOKEN ne porte plus aucune écriture : le token de l'App s'en charge.
22
+ contents: read
23
+ outputs:
24
+ release_created: ${{ steps.release.outputs.release_created }}
25
+ tag_name: ${{ steps.release.outputs.tag_name }}
26
+ steps:
27
+ # release-please s'authentifie en GitHub App plutôt qu'avec le GITHUB_TOKEN, pour deux
28
+ # raisons. Ce dernier se voit refuser la création de PR tant que le réglage « Allow
29
+ # GitHub Actions to create and approve pull requests » du dépôt est décoché ; et surtout
30
+ # ses écritures ne déclenchent aucun workflow, si bien que la PR de release n'obtiendrait
31
+ # jamais les checks que le ruleset de main exige et resterait infusionnable. Une App est
32
+ # une identité distincte : ses événements déclenchent la CI, et le token d'installation
33
+ # qu'on en dérive expire au bout d'une heure.
34
+ - name: Mint a GitHub App token
35
+ id: app-token
36
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
37
+ with:
38
+ client-id: ${{ secrets.RELEASE_PLEASE_CLIENT_ID }}
39
+ private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }}
40
+
41
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
42
+ id: release
43
+ with:
44
+ token: ${{ steps.app-token.outputs.token }}
45
+ config-file: release-please-config.json
46
+ manifest-file: .release-please-manifest.json
47
+
48
+ # release-please bump la version dans pyproject.toml mais ignore uv.lock, qui porte lui
49
+ # aussi la version du paquet. Sans resynchronisation, le lockfile est périmé dès la
50
+ # release et le premier `uv run` venu le réécrit. On corrige sur la branche de la PR de
51
+ # release, avant sa fusion, pour que main ne reçoive jamais de lockfile désaligné.
52
+ # `uv lock` relit pyproject.toml : la règle survit à tout changement de schéma de version.
53
+ - name: Check out the release pull request branch
54
+ if: steps.release.outputs.pr
55
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
56
+ with:
57
+ ref: ${{ fromJson(steps.release.outputs.pr).headBranchName }}
58
+
59
+ - name: Set up uv
60
+ if: steps.release.outputs.pr
61
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
62
+
63
+ - name: Sync uv.lock with the bumped version
64
+ if: steps.release.outputs.pr
65
+ env:
66
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
67
+ # Le court-circuit n'est pas décoratif : les expressions d'un bloc `env:` de step
68
+ # sont évaluées avant son `if:`, à la différence de celles d'un `with:`. Sur les runs
69
+ # qui publient une release au lieu d'ouvrir une PR, `steps.release.outputs.pr` est
70
+ # vide ; sans garde, `fromJson` recevrait la chaîne vide et ferait échouer le step
71
+ # alors même qu'il doit être sauté.
72
+ BRANCH: ${{ steps.release.outputs.pr && fromJson(steps.release.outputs.pr).headBranchName || '' }}
73
+ run: |
74
+ uv lock
75
+ if git diff --quiet uv.lock; then
76
+ echo "uv.lock already in sync"
77
+ exit 0
78
+ fi
79
+ # Le commit passe par l'API et non par `git commit` : le ruleset de main exige des
80
+ # signatures vérifiées, or un commit fabriqué dans le runner n'est pas signé et
81
+ # bloquerait la fusion de la PR de release (« Commits must have verified
82
+ # signatures »). GitHub signe de lui-même les commits créés via son API, comme il
83
+ # le fait déjà pour ceux de release-please. `sha` est le blob du fichier tel qu'il
84
+ # est sur la branche : `uv lock` a modifié la copie de travail, pas HEAD.
85
+ gh api -X PUT "repos/${{ github.repository }}/contents/uv.lock" \
86
+ -f message="chore: sync uv.lock with the release version" \
87
+ -f branch="$BRANCH" \
88
+ -f sha="$(git rev-parse HEAD:uv.lock)" \
89
+ -f content="$(base64 -w0 uv.lock)"
90
+
91
+ # `uv build` exécute le backend hatchling et ses dépendances, soit du code tiers. Ce job
92
+ # est donc volontairement dépourvu de `id-token: write` : une dépendance de build
93
+ # compromise n'y trouve aucun jeton OIDC dont elle pourrait se servir pour publier.
94
+ # Les distributions sont aussi attachées à la GitHub Release, d'où elles restent
95
+ # installables hors index (`pip install <url du .whl>`).
96
+ build:
97
+ name: Build distributions
98
+ needs: release-please
99
+ if: needs.release-please.outputs.release_created == 'true'
100
+ runs-on: ubuntu-latest
101
+ permissions:
102
+ contents: write
103
+ steps:
104
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
105
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
106
+ with:
107
+ enable-cache: true
108
+ - name: Build sdist and wheel
109
+ run: uv build
110
+ - name: Attach them to the release
111
+ env:
112
+ GH_TOKEN: ${{ github.token }}
113
+ TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
114
+ run: gh release upload "$TAG_NAME" dist/*
115
+ - name: Hand the distributions over to the publishing job
116
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
117
+ with:
118
+ name: dist
119
+ path: dist/
120
+ if-no-files-found: error
121
+
122
+ # Trusted Publishing : PyPI authentifie le workflow sur le jeton OIDC que GitHub lui
123
+ # délivre, il n'y a donc aucun token d'API à stocker en secret.
124
+ # Ce job détient ce jeton, il est donc réduit au strict minimum : pas de checkout, pas de
125
+ # dépendance, aucun code du dépôt — seulement les distributions déjà construites et
126
+ # l'action de publication. Rien de ce qui s'y exécute ne provient d'une PR.
127
+ publish:
128
+ name: Publish to PyPI
129
+ needs: build
130
+ runs-on: ubuntu-latest
131
+ permissions:
132
+ id-token: write
133
+ steps:
134
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
135
+ with:
136
+ name: dist
137
+ path: dist/
138
+ # L'action refuse de réécrire une version déjà présente sur PyPI : un rejeu du
139
+ # workflow échoue au lieu de corrompre.
140
+ - name: Publish to PyPI
141
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,222 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # Captures réseau brutes : elles portent les adresses MAC du matériel réel.
221
+ # Elles restent en local, jamais dans le dépôt. Voir la section « Méthode » de la spec.
222
+ records/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.0"
3
+ }
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-21)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * the package is now `pyhaseiq`, not `haseiq_connect`, and its API is asynchronous. `get_minimal_temp_percent()` is now `get_heat_up_percent()`, and `get_phase()` returns a `Phase` enum.
9
+
10
+ ### Features
11
+
12
+ * rewrite as pyhaseiq, a read-only asynchronous library ([5957084](https://github.com/bbayszczak/pyhaseiq/commit/59570847858d51b1fe2591e22260dca1cff2fe84))
13
+
14
+
15
+ ### Documentation
16
+
17
+ * use the manufacturer's own spelling for its name and products ([#2](https://github.com/bbayszczak/pyhaseiq/issues/2)) ([dbd12c4](https://github.com/bbayszczak/pyhaseiq/commit/dbd12c46514735a6c388027a10e0d08e7718dc9e))
@@ -0,0 +1,143 @@
1
+ # CLAUDE.md
2
+
3
+ Contexte pour Claude Code sur ce dépôt.
4
+
5
+ ## Le projet
6
+
7
+ `pyhaseiq` est une bibliothèque **en lecture seule** pour les poêles à bois **Hase iQ de la
8
+ génération `flamemonitor`**, via leur WebSocket local. Rien d'autre.
9
+
10
+ ⚠️ **Deux générations portent le nom « Hase iQ ».** Seule l'ancienne, pilotée par l'application
11
+ `flamemonitor`, est supportée ; celle de l'application `HASE iQ` n'a jamais été observée. Ne
12
+ jamais laisser entendre dans la documentation ou le code que la nouvelle fonctionne.
13
+
14
+ Le protocole est intégralement décrit dans [`docs/SPEC-PROTOCOLE-WS.md`](docs/SPEC-PROTOCOLE-WS.md),
15
+ reconstitué par rétro-ingénierie à partir de captures réseau qui **ne sont pas versionnées** :
16
+ elles portent les adresses MAC du matériel réel. Chaque affirmation y porte un statut ✅ validé / 🟡 partiel / ❓ supposé : s'y référer avant
17
+ d'implémenter quoi que ce soit, et ne jamais coder sur la foi d'un point ❓.
18
+
19
+ ## Structure
20
+
21
+ ```
22
+ src/pyhaseiq/
23
+ protocol.py encodage base64 et parsing — fonctions pures, aucune I/O
24
+ client.py client asynchrone, sérialise le dialogue
25
+ models.py Phase
26
+ exceptions.py
27
+ tests/
28
+ fake.py faux poêle (serveur WebSocket) rejouant les réponses réelles
29
+ docs/
30
+ SPEC-PROTOCOLE-WS.md
31
+ tools/
32
+ extract.py décode un export Wireshark en dialogue lisible
33
+ demo.py script de démonstration, lecture seule
34
+ ```
35
+
36
+ ## Commandes
37
+
38
+ ```bash
39
+ uv run ruff check . # lint
40
+ uv run ruff format . # formatage
41
+ uv run pytest # tests, sans matériel
42
+ uv run demo.py <ip> # lecture live sur un vrai poêle
43
+ ```
44
+
45
+ Toujours passer par `uv`. Python ≥ 3.13, CI sur 3.13 et 3.14.
46
+
47
+ ## Conventions
48
+
49
+ - **Commits en Conventional Commits**, en anglais. `release-please` s'en sert pour produire le
50
+ CHANGELOG et la version : seuls `feat:` et `fix:` déclenchent une release.
51
+ - Documentation en français, code et docstrings en anglais.
52
+ - **Logging** : `logging` standard, un `_LOGGER = logging.getLogger(__name__)` par module et
53
+ **aucune configuration** (ni handler, ni niveau, ni format) — l'hôte, typiquement Home
54
+ Assistant, possède les handlers et filtre sur `pyhaseiq.<module>`. Tout en `DEBUG`, en
55
+ formatage paresseux (`_LOGGER.debug("%s = %s", name, value)`), jamais de f-string — les
56
+ règles ruff `LOG` et `G` le vérifient. Les erreurs se lèvent, elles ne se loguent pas :
57
+ loguer *et* lever produit un doublon dans les journaux de l'appelant.
58
+ - Le linter est strict (docstrings et annotations obligatoires dans `src/`) ; les tests en sont
59
+ dispensés via `per-file-ignores`.
60
+ - Les actions GitHub sont **épinglées sur des SHA complets** (un tag comme `@v4` peut être
61
+ redéplacé sur un autre commit). Dependabot les met à jour ; ne jamais revenir à un tag mobile.
62
+ - Dépôt public : `CONTRIBUTING.md` et `SECURITY.md` font foi côté contributeurs, les garder
63
+ cohérents avec ce fichier — notamment la liste des interdits ci-dessous.
64
+ - `uv.lock` porte la version du paquet : le workflow de release le resynchronise sur la branche
65
+ de la PR de release. Ne pas l'éditer à la main, lancer `uv lock` après tout changement de
66
+ version ou de dépendance.
67
+ - La fusion d'une PR de release publie le paquet sur **PyPI** via le *Trusted Publishing*
68
+ (OIDC) : aucun token d'API n'est stocké, l'autorisation vit dans le *publisher* déclaré côté
69
+ PyPI (dépôt `bbayszczak/pyhaseiq`, workflow `release.yml`). Renommer ce fichier ou le dépôt
70
+ casse la publication tant que le *publisher* n'est pas mis à jour.
71
+ - Le workflow de release sépare volontairement `build` et `publish` : `uv build` exécute du
72
+ code tiers (hatchling et ses dépendances) et ne doit jamais tourner dans le job qui porte
73
+ `id-token: write`, sans quoi une dépendance de build compromise pourrait publier sur PyPI.
74
+ Ne pas refusionner ces deux jobs.
75
+ - `release-please` tourne sous l'identité d'une **GitHub App** dédiée, jamais sous le
76
+ `GITHUB_TOKEN` : celui-ci ne peut pas ouvrir de PR tant que le réglage « Allow GitHub Actions
77
+ to create and approve pull requests » du dépôt est décoché, et ses écritures ne déclenchent
78
+ aucun workflow — la PR de release n'obtiendrait donc jamais les checks que le ruleset de
79
+ `main` exige et resterait infusionnable. Ses identifiants vivent dans les secrets
80
+ `RELEASE_PLEASE_CLIENT_ID` et `RELEASE_PLEASE_PRIVATE_KEY`. Le premier porte le **Client
81
+ ID** de l'App (`Iv23li…`), pas son App ID numérique : l'entrée `app-id` de l'action est
82
+ dépréciée et `client-id` attend l'autre valeur.
83
+ - Le commit qui resynchronise `uv.lock` est créé par **l'API GitHub**, jamais par un
84
+ `git commit` dans le *runner* : `main` exige des signatures vérifiées, or un commit fabriqué
85
+ sur le *runner* n'est pas signé et bloque la fusion de la PR de release. GitHub signe les
86
+ commits passés par son API, et l'appel porte le token de l'App, donc la CI se redéclenche.
87
+
88
+ ## Principes de conception
89
+
90
+ - **Lecture seule, définitivement.** Aucune commande d'écriture n'a été observée dans le
91
+ protocole et aucune n'est implémentée. C'est la garantie centrale du projet : une
92
+ bibliothèque qui ne fait que lire ne peut rien casser sur un appareil à combustion installé
93
+ chez quelqu'un. Ne jamais l'entamer, même « juste pour tester ».
94
+ - **Le client ne connaît aucun état.** Pas de cache, pas d'historique, pas de reconnexion
95
+ automatique. Une connexion perdue reste perdue et l'appelant en ouvre une neuve. Toute
96
+ historisation, moyenne ou politique de reprise appartient à la couche appelante.
97
+ - **Dialogue strictement sérialisé.** Le protocole n'a aucun identifiant de corrélation : rien
98
+ ne rattache une réponse à sa requête sinon l'ordre. D'où le verrou dans `Client.get()`. Le
99
+ test `test_concurrent_readings_are_serialised_and_never_swap_answers` garde la propriété — il
100
+ échoue si on retire le verrou.
101
+ - **Cœur asynchrone assumé** : le poêle est un serveur WebSocket et la cible est Home
102
+ Assistant. Pas de façade synchrone.
103
+
104
+ ## Pièges du protocole
105
+
106
+ - **Le préfixe se retire avec `removeprefix`, jamais avec `lstrip`.** `lstrip` prend un
107
+ *ensemble de caractères* : `"appT=appT".lstrip("appT=")` rend `""`. C'est un bug réel corrigé
108
+ ici, gardé par `test_the_prefix_is_removed_as_a_prefix_not_as_a_character_set`.
109
+ - **La valeur peut contenir un `=`** : `_oemver` répond `_oemver=AAF_5815=9`. Ne découper que
110
+ sur le premier.
111
+ - **Toutes les mesures ne sont pas lisibles dans toutes les phases.** L'application
112
+ constructeur ne demande `appT` et `appAufheiz` qu'en phase `HEATING_UP`, `appP` qu'en phase
113
+ `NOMINAL`. On ignore si le poêle répond hors phase ou se tait — auquel cas l'appel part au
114
+ `ResponseTimeoutError`. Tester la phase avant de lire.
115
+ - **La phase `4` n'a jamais été observée** : elle vient de la documentation du poêle.
116
+
117
+ ## À ne pas faire
118
+
119
+ - ⛔ **Ne jamais implémenter ni envoyer de commande d'écriture** vers le poêle.
120
+ - ⛔ **Ne pas balayer de noms de requêtes au hasard** sur un poêle réel : on ignore ce qu'un
121
+ `_req=` inconnu déclenche dans le micrologiciel.
122
+ - ⛔ **Ne pas recalculer `appAufheiz` à partir de `appT`.** La corrélation est forte
123
+ (R² = 0,992) mais pas exacte : le poêle y mêle autre chose.
124
+ - ⛔ **Ne pas présenter les valeurs lues comme un dispositif de sécurité** — ni dans le code,
125
+ ni dans la documentation. Elles sont indicatives.
126
+ - ⛔ **Ne jamais committer de capture réseau brute** — `.pcap`, export Wireshark, ou sortie de
127
+ `tools/extract.py`. Une capture Ethernet porte les **adresses MAC** du poêle et du téléphone,
128
+ qui sont des identifiants matériels permanents. `records/` est dans `.gitignore` pour cette
129
+ raison ; ne pas l'en retirer.
130
+
131
+ ## Sécurité
132
+
133
+ Le poêle n'a **aucune authentification** : son WebSocket est ouvert à tout le réseau local.
134
+ C'est un fait matériel, pas une faille de cette bibliothèque. La documentation doit le dire et
135
+ rappeler de ne jamais exposer le port `8080` sur Internet.
136
+
137
+ Les traces ne contiennent ni identifiant, ni clé, ni donnée personnelle : il n'y a rien à
138
+ masquer dans les logs, contrairement à d'autres protocoles domotiques. Si une requête porteuse
139
+ de secret apparaissait un jour, ce constat serait à revoir.
140
+
141
+ Cela vaut pour les traces de la bibliothèque, **pas pour les captures réseau** dont la spec est
142
+ issue : au niveau Ethernet, elles portent les adresses MAC du matériel. Elles restent hors du
143
+ dépôt.