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.
- pyhaseiq-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +58 -0
- pyhaseiq-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- pyhaseiq-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +29 -0
- pyhaseiq-0.1.0/.github/dependabot.yml +12 -0
- pyhaseiq-0.1.0/.github/workflows/ci.yml +44 -0
- pyhaseiq-0.1.0/.github/workflows/release.yml +141 -0
- pyhaseiq-0.1.0/.gitignore +222 -0
- pyhaseiq-0.1.0/.release-please-manifest.json +3 -0
- pyhaseiq-0.1.0/CHANGELOG.md +17 -0
- pyhaseiq-0.1.0/CLAUDE.md +143 -0
- pyhaseiq-0.1.0/CONTRIBUTING.md +74 -0
- pyhaseiq-0.1.0/LICENSE +21 -0
- pyhaseiq-0.1.0/PKG-INFO +354 -0
- pyhaseiq-0.1.0/README.md +331 -0
- pyhaseiq-0.1.0/SECURITY.md +31 -0
- pyhaseiq-0.1.0/demo.py +133 -0
- pyhaseiq-0.1.0/docs/SPEC-PROTOCOLE-WS.md +215 -0
- pyhaseiq-0.1.0/pyproject.toml +83 -0
- pyhaseiq-0.1.0/release-please-config.json +12 -0
- pyhaseiq-0.1.0/src/pyhaseiq/__init__.py +45 -0
- pyhaseiq-0.1.0/src/pyhaseiq/client.py +173 -0
- pyhaseiq-0.1.0/src/pyhaseiq/exceptions.py +31 -0
- pyhaseiq-0.1.0/src/pyhaseiq/models.py +25 -0
- pyhaseiq-0.1.0/src/pyhaseiq/protocol.py +66 -0
- pyhaseiq-0.1.0/src/pyhaseiq/py.typed +0 -0
- pyhaseiq-0.1.0/tests/__init__.py +0 -0
- pyhaseiq-0.1.0/tests/fake.py +78 -0
- pyhaseiq-0.1.0/tests/test_client.py +102 -0
- pyhaseiq-0.1.0/tests/test_protocol.py +64 -0
- pyhaseiq-0.1.0/tools/extract.py +59 -0
- pyhaseiq-0.1.0/uv.lock +271 -0
|
@@ -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,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,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))
|
pyhaseiq-0.1.0/CLAUDE.md
ADDED
|
@@ -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.
|