pyfenixcontrol 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .coverage
8
+ coverage.xml
9
+ htmlcov/
10
+ .mypy_cache/
11
+ .pytest_cache/
12
+ .ruff_cache/
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-10-15
8
+
9
+ First release, extracted from the `api.py` module of the
10
+ [`fenix_tft`](https://github.com/baracudaz/fenix_tft) Home Assistant integration
11
+ (v1.3.8).
12
+
13
+ ### Added
14
+ - `FenixControlClient` with typed models (`Installation`, `Room`, `Device`,
15
+ `EnergyMetric`) and enums (`PresetMode`, `HvacState`, `HolidayMode`,
16
+ `EnergyPeriod`).
17
+ - Pluggable authentication via `AbstractAuth`; `PasswordAuth` implements the
18
+ existing e-mail/password OAuth2 PKCE login.
19
+ - Exception hierarchy separating auth, connection and response errors.
20
+
21
+ ### Changed compared to the integration's `api.py`
22
+ - No dependency on Home Assistant or BeautifulSoup (login form parsed with the
23
+ standard library).
24
+ - Network errors and timeouts during login raise `FenixControlConnectionError`
25
+ instead of an auth error, so a network outage no longer looks like invalid
26
+ credentials.
27
+ - Invalid credentials are reported explicitly instead of a generic login failure.
28
+ - Concurrent requests share one login/refresh (asyncio lock) instead of a
29
+ re-entrancy flag that let parallel callers proceed without a token.
30
+ - Identity-server cookies are cleared before a full login, so a cached identity
31
+ session cannot authenticate a different account sharing the HTTP session.
32
+ - PUT requests now use the request timeout (previously unbounded).
33
+ - API requests no longer follow HTTP redirects.
34
+ - Energy queries take explicit `start`/`end` instead of "today in HA's time zone";
35
+ the daily total and the "user connected" update trigger are left to the caller.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Stanislav Likavčan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyfenixcontrol
3
+ Version: 0.1.0
4
+ Summary: Asynchronous Python client for the FENIX Control cloud API (FENIX TFT WiFi thermostats).
5
+ Project-URL: Homepage, https://github.com/baracudaz/pyfenixcontrol
6
+ Project-URL: Source, https://github.com/baracudaz/pyfenixcontrol
7
+ Project-URL: Issues, https://github.com/baracudaz/pyfenixcontrol/issues
8
+ Project-URL: Changelog, https://github.com/baracudaz/pyfenixcontrol/blob/main/CHANGELOG.md
9
+ Author-email: Stanislav Likavčan <likavcan@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: asyncio,fenix,fenix control,home assistant,tft,thermostat
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Home Automation
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: aiohttp>=3.9.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # pyfenixcontrol
28
+
29
+ Asynchronous Python client for the **FENIX Control** cloud, used by FENIX TFT WiFi
30
+ thermostats. It powers the FENIX integration for [Home Assistant](https://www.home-assistant.io/),
31
+ but has no Home Assistant dependency and can be used from any asyncio code.
32
+
33
+ - Fully typed (`py.typed`), `asyncio` + `aiohttp`, Python 3.12+
34
+ - Read thermostats, rooms and installations; set target temperature and operating mode
35
+ - Installation-wide holiday schedules
36
+ - Room energy consumption at hourly … yearly aggregation
37
+ - Pluggable authentication: bundled e-mail/password login, or bring your own OAuth2 tokens
38
+ - Automatic token refresh, single retry on HTTP 401, backoff on HTTP 5xx
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install pyfenixcontrol
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```python
49
+ import asyncio
50
+
51
+ import aiohttp
52
+
53
+ from pyfenixcontrol import FenixControlClient, PasswordAuth, PresetMode
54
+
55
+
56
+ async def main() -> None:
57
+ async with aiohttp.ClientSession() as session:
58
+ auth = PasswordAuth(session, "you@example.com", "password")
59
+ client = FenixControlClient(auth)
60
+
61
+ for device in await client.get_devices():
62
+ print(device.name, device.room_temperature, device.target_temperature)
63
+
64
+ await client.set_target_temperature(device.id, 21.5)
65
+ await client.set_preset_mode(device.id, PresetMode.PROGRAM)
66
+
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ ## Authentication
72
+
73
+ The client never sees credentials. It asks an `AbstractAuth` for a bearer token:
74
+
75
+ ```python
76
+ from pyfenixcontrol import AbstractAuth
77
+
78
+
79
+ class MyAuth(AbstractAuth):
80
+ async def async_get_access_token(self) -> str:
81
+ return await my_token_store.get_valid_token()
82
+
83
+ async def async_invalidate_access_token(self) -> None:
84
+ # Called after an HTTP 401; optional.
85
+ await my_token_store.mark_expired()
86
+ ```
87
+
88
+ `PasswordAuth` performs the OAuth2 authorization-code + PKCE login used by the
89
+ FENIX Control app and keeps tokens in memory. `await auth.async_login()` validates
90
+ credentials up front (e.g. in a setup wizard).
91
+
92
+ `PasswordAuth` needs an `aiohttp` session with a working cookie jar, because the
93
+ identity server tracks its login form with cookies. Cookies of the identity host
94
+ are cleared before each full login.
95
+
96
+ ## API overview
97
+
98
+ | Method | Description |
99
+ |---|---|
100
+ | `get_user_id()` | Account subject id (cached on the client) |
101
+ | `get_installations()` | Installations (homes) with their rooms and device ids |
102
+ | `get_devices(installations=None)` | `Device` state of every thermostat |
103
+ | `get_device_properties(device_id)` | Raw device twin properties |
104
+ | `trigger_installation_update(installation_id)` | Ask thermostats to report fresh values |
105
+ | `set_target_temperature(device_id, celsius)` | Set target °C (switches to manual) |
106
+ | `set_preset_mode(device_id, mode)` | `PresetMode.OFF / PROGRAM / MANUAL / DEFROST / BOOST / HOLIDAYS` |
107
+ | `set_holiday_schedule(installation_id, start, end, mode)` | Holiday for the whole installation |
108
+ | `cancel_holiday_schedule(installation_id)` | Remove the holiday schedule |
109
+ | `get_room_energy(installation_id, room_id, start, end, period)` | `EnergyMetric` buckets in Wh |
110
+
111
+ ### Errors
112
+
113
+ | Exception | Meaning |
114
+ |---|---|
115
+ | `FenixControlAuthError` | Credentials or tokens rejected – user action needed |
116
+ | `FenixControlConnectionError` | Network error or timeout – retry later |
117
+ | `FenixControlResponseError` | Unexpected status or body (`.status` holds the HTTP code) |
118
+
119
+ All inherit from `FenixControlError`.
120
+
121
+ ### Units and conventions
122
+
123
+ - Temperatures are °C. The API transports °F × `divFactor`;
124
+ `decode_temperature()` / `encode_temperature()` convert.
125
+ - Holiday `start`/`end` are wall-clock times of the installation (no time zone
126
+ conversion). Energy queries convert to UTC; naive datetimes are taken as UTC.
127
+ - Every model keeps the original payload in `.raw` for fields not yet modelled.
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ uv sync
133
+ uv run pytest --cov
134
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
135
+ ```
136
+
137
+ Tests run against a local fake of the cloud and identity server; no account is needed.
138
+
139
+ Releases: bump `__version__`, update `CHANGELOG.md`, publish a GitHub release
140
+ tagged `vX.Y.Z`. The release workflow publishes to PyPI via trusted publishing.
141
+
142
+ ## License
143
+
144
+ MIT – see [LICENSE](LICENSE). Not an official product of FENIX Trading s.r.o.
145
+ unless stated otherwise in the project metadata.
@@ -0,0 +1,119 @@
1
+ # pyfenixcontrol
2
+
3
+ Asynchronous Python client for the **FENIX Control** cloud, used by FENIX TFT WiFi
4
+ thermostats. It powers the FENIX integration for [Home Assistant](https://www.home-assistant.io/),
5
+ but has no Home Assistant dependency and can be used from any asyncio code.
6
+
7
+ - Fully typed (`py.typed`), `asyncio` + `aiohttp`, Python 3.12+
8
+ - Read thermostats, rooms and installations; set target temperature and operating mode
9
+ - Installation-wide holiday schedules
10
+ - Room energy consumption at hourly … yearly aggregation
11
+ - Pluggable authentication: bundled e-mail/password login, or bring your own OAuth2 tokens
12
+ - Automatic token refresh, single retry on HTTP 401, backoff on HTTP 5xx
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install pyfenixcontrol
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ import asyncio
24
+
25
+ import aiohttp
26
+
27
+ from pyfenixcontrol import FenixControlClient, PasswordAuth, PresetMode
28
+
29
+
30
+ async def main() -> None:
31
+ async with aiohttp.ClientSession() as session:
32
+ auth = PasswordAuth(session, "you@example.com", "password")
33
+ client = FenixControlClient(auth)
34
+
35
+ for device in await client.get_devices():
36
+ print(device.name, device.room_temperature, device.target_temperature)
37
+
38
+ await client.set_target_temperature(device.id, 21.5)
39
+ await client.set_preset_mode(device.id, PresetMode.PROGRAM)
40
+
41
+
42
+ asyncio.run(main())
43
+ ```
44
+
45
+ ## Authentication
46
+
47
+ The client never sees credentials. It asks an `AbstractAuth` for a bearer token:
48
+
49
+ ```python
50
+ from pyfenixcontrol import AbstractAuth
51
+
52
+
53
+ class MyAuth(AbstractAuth):
54
+ async def async_get_access_token(self) -> str:
55
+ return await my_token_store.get_valid_token()
56
+
57
+ async def async_invalidate_access_token(self) -> None:
58
+ # Called after an HTTP 401; optional.
59
+ await my_token_store.mark_expired()
60
+ ```
61
+
62
+ `PasswordAuth` performs the OAuth2 authorization-code + PKCE login used by the
63
+ FENIX Control app and keeps tokens in memory. `await auth.async_login()` validates
64
+ credentials up front (e.g. in a setup wizard).
65
+
66
+ `PasswordAuth` needs an `aiohttp` session with a working cookie jar, because the
67
+ identity server tracks its login form with cookies. Cookies of the identity host
68
+ are cleared before each full login.
69
+
70
+ ## API overview
71
+
72
+ | Method | Description |
73
+ |---|---|
74
+ | `get_user_id()` | Account subject id (cached on the client) |
75
+ | `get_installations()` | Installations (homes) with their rooms and device ids |
76
+ | `get_devices(installations=None)` | `Device` state of every thermostat |
77
+ | `get_device_properties(device_id)` | Raw device twin properties |
78
+ | `trigger_installation_update(installation_id)` | Ask thermostats to report fresh values |
79
+ | `set_target_temperature(device_id, celsius)` | Set target °C (switches to manual) |
80
+ | `set_preset_mode(device_id, mode)` | `PresetMode.OFF / PROGRAM / MANUAL / DEFROST / BOOST / HOLIDAYS` |
81
+ | `set_holiday_schedule(installation_id, start, end, mode)` | Holiday for the whole installation |
82
+ | `cancel_holiday_schedule(installation_id)` | Remove the holiday schedule |
83
+ | `get_room_energy(installation_id, room_id, start, end, period)` | `EnergyMetric` buckets in Wh |
84
+
85
+ ### Errors
86
+
87
+ | Exception | Meaning |
88
+ |---|---|
89
+ | `FenixControlAuthError` | Credentials or tokens rejected – user action needed |
90
+ | `FenixControlConnectionError` | Network error or timeout – retry later |
91
+ | `FenixControlResponseError` | Unexpected status or body (`.status` holds the HTTP code) |
92
+
93
+ All inherit from `FenixControlError`.
94
+
95
+ ### Units and conventions
96
+
97
+ - Temperatures are °C. The API transports °F × `divFactor`;
98
+ `decode_temperature()` / `encode_temperature()` convert.
99
+ - Holiday `start`/`end` are wall-clock times of the installation (no time zone
100
+ conversion). Energy queries convert to UTC; naive datetimes are taken as UTC.
101
+ - Every model keeps the original payload in `.raw` for fields not yet modelled.
102
+
103
+ ## Development
104
+
105
+ ```bash
106
+ uv sync
107
+ uv run pytest --cov
108
+ uv run ruff check . && uv run ruff format --check . && uv run mypy
109
+ ```
110
+
111
+ Tests run against a local fake of the cloud and identity server; no account is needed.
112
+
113
+ Releases: bump `__version__`, update `CHANGELOG.md`, publish a GitHub release
114
+ tagged `vX.Y.Z`. The release workflow publishes to PyPI via trusted publishing.
115
+
116
+ ## License
117
+
118
+ MIT – see [LICENSE](LICENSE). Not an official product of FENIX Trading s.r.o.
119
+ unless stated otherwise in the project metadata.
@@ -0,0 +1,87 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyfenixcontrol"
7
+ dynamic = ["version"]
8
+ description = "Asynchronous Python client for the FENIX Control cloud API (FENIX TFT WiFi thermostats)."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ authors = [{ name = "Stanislav Likavčan", email = "likavcan@gmail.com" }]
14
+ keywords = ["fenix", "fenix control", "tft", "thermostat", "home assistant", "asyncio"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Framework :: AsyncIO",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Home Automation",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = ["aiohttp>=3.9.0"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/baracudaz/pyfenixcontrol"
31
+ Source = "https://github.com/baracudaz/pyfenixcontrol"
32
+ Issues = "https://github.com/baracudaz/pyfenixcontrol/issues"
33
+ Changelog = "https://github.com/baracudaz/pyfenixcontrol/blob/main/CHANGELOG.md"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "mypy>=1.11",
38
+ "pytest>=8.0",
39
+ "pytest-asyncio>=0.24",
40
+ "pytest-cov>=5.0",
41
+ "ruff>=0.6",
42
+ ]
43
+
44
+ [tool.hatch.version]
45
+ path = "src/pyfenixcontrol/__init__.py"
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["src/pyfenixcontrol", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ asyncio_default_fixture_loop_scope = "function"
53
+ testpaths = ["tests"]
54
+ addopts = "--strict-markers"
55
+
56
+ [tool.coverage.run]
57
+ source = ["pyfenixcontrol"]
58
+ branch = true
59
+
60
+ [tool.coverage.report]
61
+ show_missing = true
62
+ exclude_also = ["if TYPE_CHECKING:", "@abstractmethod", "raise NotImplementedError"]
63
+
64
+ [tool.mypy]
65
+ python_version = "3.12"
66
+ strict = true
67
+ warn_unreachable = true
68
+ files = ["src", "tests"]
69
+
70
+ [tool.ruff]
71
+ target-version = "py312"
72
+ line-length = 88
73
+
74
+ [tool.ruff.lint]
75
+ select = ["ALL"]
76
+ ignore = [
77
+ "COM812", # conflicts with formatter
78
+ "D203", # incompatible with D211
79
+ "D213", # incompatible with D212
80
+ "PLR0913", # API methods legitimately take many arguments
81
+ "CPY001", # license is declared once in LICENSE
82
+ "ANN401", # JSON payloads are genuinely untyped at the API boundary
83
+ "B027", # AbstractAuth.async_invalidate_access_token is an optional hook
84
+ ]
85
+
86
+ [tool.ruff.lint.per-file-ignores]
87
+ "tests/**" = ["S101", "S105", "PLR2004", "SLF001", "D", "FBT001", "DTZ001", "TC001", "TC003"]
@@ -0,0 +1,46 @@
1
+ """Asynchronous Python client for the FENIX Control cloud API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .auth import AbstractAuth, PasswordAuth
6
+ from .client import FenixControlClient
7
+ from .const import EnergyPeriod, HolidayMode, HvacState, PresetMode
8
+ from .exceptions import (
9
+ FenixControlAuthError,
10
+ FenixControlConnectionError,
11
+ FenixControlError,
12
+ FenixControlResponseError,
13
+ )
14
+ from .models import (
15
+ Device,
16
+ EnergyMetric,
17
+ Installation,
18
+ Room,
19
+ decode_temperature,
20
+ encode_temperature,
21
+ parse_holiday_date,
22
+ )
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = [
27
+ "AbstractAuth",
28
+ "Device",
29
+ "EnergyMetric",
30
+ "EnergyPeriod",
31
+ "FenixControlAuthError",
32
+ "FenixControlClient",
33
+ "FenixControlConnectionError",
34
+ "FenixControlError",
35
+ "FenixControlResponseError",
36
+ "HolidayMode",
37
+ "HvacState",
38
+ "Installation",
39
+ "PasswordAuth",
40
+ "PresetMode",
41
+ "Room",
42
+ "__version__",
43
+ "decode_temperature",
44
+ "encode_temperature",
45
+ "parse_holiday_date",
46
+ ]