easee-ble 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,5 @@
1
+ # Changelog
2
+
3
+ Notable changes per release. Versions follow [SemVer](https://semver.org).
4
+
5
+ ## Unreleased
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 parrel
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,5 @@
1
+ # The sdist ships the tests, so include the data they need - without this,
2
+ # `pytest` against an unpacked sdist fails on a missing vector file.
3
+ recursive-include tests *.json
4
+
5
+ include CHANGELOG.md
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: easee-ble
3
+ Version: 0.1.0
4
+ Summary: Local Bluetooth control for Easee EV chargers
5
+ Author: parrel
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/parrel/easee-ble
8
+ Project-URL: Bug Tracker, https://github.com/parrel/easee-ble/issues
9
+ Keywords: easee,ev,charger,bluetooth,ble,home-assistant
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Framework :: AsyncIO
17
+ Classifier: Topic :: Home Automation
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pycryptodome>=3.20
22
+ Requires-Dist: ecdsa>=0.19
23
+ Requires-Dist: bleak>=0.22
24
+ Requires-Dist: bleak-retry-connector>=3.5.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
28
+ Requires-Dist: pytest-cov>=5; extra == "dev"
29
+ Requires-Dist: ruff>=0.6; extra == "dev"
30
+ Requires-Dist: mypy>=1.11; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # easee-ble
34
+
35
+ Local **Bluetooth** control for [Easee](https://easee.com) EV chargers. Talk to the charger directly with the PIN printed on the unit.
36
+
37
+ ```bash
38
+ pip install easee-ble
39
+ ```
40
+
41
+ Requires Python 3.11+. For Home Assistant, use
42
+ [ha-easee-ble](https://github.com/parrel/ha-easee-ble), which is built on this.
43
+
44
+ ## Quick start
45
+
46
+ ```python
47
+ import asyncio
48
+ from bleak import BleakScanner
49
+ from easee_ble import EaseeCharger, PhaseMode
50
+
51
+ async def main():
52
+ device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
53
+ charger = EaseeCharger(device, pin="1234", serial="EMX00000")
54
+
55
+ await charger.connect() # handshake; the link is held open
56
+ print(await charger.poll()) # {'chargerOpMode': 3, 'totalPower': 2.69, ...}
57
+
58
+ await charger.perform(lambda s: s.set_charger_enabled(True))
59
+ await charger.perform(lambda s: s.set_phase_mode(PhaseMode.AUTO))
60
+
61
+ await charger.disconnect()
62
+
63
+ asyncio.run(main())
64
+ ```
65
+
66
+ You need two things from the charger: the **PIN** printed on the unit, and its
67
+ **serial** (`EMXXXXXX`, also on the unit and in the Easee app).
68
+
69
+ ### Finding your charger
70
+
71
+ Scan by service UUID if you do not know the address:
72
+
73
+ ```python
74
+ from easee_ble import SERVICE_UUID
75
+
76
+ device = await BleakScanner.find_device_by_filter(
77
+ lambda d, adv: SERVICE_UUID in adv.service_uuids
78
+ )
79
+ ```
80
+
81
+ Chargers usually advertise only **intermittently**. To get one to show up,
82
+ either set Bluetooth to always-on in the Easee app, or open a window with a long
83
+ press of the charger's touch button.
84
+
85
+ ## Reading
86
+
87
+ `poll()` merges State and Config into one dict of named fields:
88
+
89
+ ```python
90
+ data = await charger.poll()
91
+ data["chargerOpMode"] # 3
92
+ data["totalPower"] # 2.69
93
+ data["maxChargerCurrent"] # 16
94
+ charger.unknown # fields we have no name for yet, by number
95
+ ```
96
+
97
+ Two fields belong together. `chargerOpMode` describes the **car and cable** - it
98
+ still reads `AWAITING_START` for a charger that has been switched off.
99
+ `reasonForNoCurrent` says **why** no current flows, and reads 53
100
+ (*charger disabled*) in exactly that case. Show both:
101
+
102
+ ```python
103
+ from easee_ble import charger_op_mode, reason_for_no_current
104
+
105
+ charger_op_mode(data["chargerOpMode"]) # ChargerOpMode.CHARGING
106
+ reason_for_no_current(data.get("reasonForNoCurrent")) # 'Charger disabled'
107
+ ```
108
+
109
+ Both return `None` on a value this library has not seen, where `ChargerOpMode()`
110
+ and `NetworkStatus()` raise. The descriptions are display text; key on
111
+ `reason_for_no_current_slug()` if you need something stable.
112
+
113
+ Fields at their default value are **absent** from the wire, not zero. `poll()`
114
+ already fills a curated set back in as `0`, so treat a missing field as unknown
115
+ rather than surprising.
116
+
117
+ ## Commands
118
+
119
+ Every command goes through `perform()`, which builds the request and checks the
120
+ answer:
121
+
122
+ ```python
123
+ await charger.perform(lambda s: s.set_charger_enabled(True))
124
+ await charger.perform(lambda s: s.set_max_charger_current(16))
125
+ await charger.perform(lambda s: s.set_dynamic_charger_current(10))
126
+ await charger.perform(lambda s: s.set_circuit_max_current(20)) # or (p1, p2, p3)
127
+ await charger.perform(lambda s: s.set_offline_max_circuit_current(10))
128
+ await charger.perform(lambda s: s.set_phase_mode(PhaseMode.LOCKED_3_PHASE))
129
+ await charger.perform(lambda s: s.set_led_brightness(75)) # 0-100
130
+ await charger.perform(lambda s: s.set_cable_locked(True))
131
+ await charger.perform(lambda s: s.set_access_control(True))
132
+ ```
133
+
134
+ `PhaseMode` is `LOCKED_1_PHASE`, `AUTO` or `LOCKED_3_PHASE`.
135
+
136
+ ### RFID / account keys
137
+
138
+ ```python
139
+ from easee_ble import command_payload
140
+
141
+ reply = await charger.perform(lambda s: s.list_user_tokens())
142
+ keys = command_payload(reply)
143
+ await charger.perform(lambda s: s.get_user_token(slot, name))
144
+ await charger.perform(lambda s: s.set_user_token(slot, name, token))
145
+ ```
146
+
147
+ ### Other reads
148
+
149
+ Besides `poll()`, individual frames are available through `perform()`:
150
+
151
+ ```python
152
+ await charger.perform(lambda s: s.poll_state())
153
+ await charger.perform(lambda s: s.poll_config())
154
+ await charger.perform(lambda s: s.poll_structure())
155
+ await charger.perform(lambda s: s.poll_debug())
156
+ ```
157
+
158
+ A reply only arrives on a subscribed channel, and the last two are not
159
+ subscribed to by default. Ask for them at connect - `EaseeCharger(...,
160
+ channels=DEFAULT_CHANNELS | {Channel.DEBUG})` - or the call raises. `poll()`
161
+ takes the same argument to read them alongside Config and State.
162
+
163
+ ## Errors
164
+
165
+ ```python
166
+ from easee_ble import EaseeCommandRefused, EaseeConnectionError, JPakeError
167
+
168
+ try:
169
+ await charger.connect()
170
+ await charger.perform(lambda s: s.set_max_charger_current(32))
171
+ except JPakeError: # almost always the wrong PIN
172
+ ...
173
+ except EaseeCommandRefused: # the charger answered, and said no
174
+ ...
175
+ except EaseeConnectionError: # connecting or talking to it failed
176
+ ...
177
+ ```
178
+
179
+ A reply is **not** an acknowledgement - the charger answers a command it refuses
180
+ just as promptly as one it accepts. `perform()` checks for you and raises
181
+ `EaseeCommandRefused`; anything else, `command_accepted()` and
182
+ `command_refusal()` check by hand.
183
+
184
+ ## Connection notes
185
+
186
+ - The connection is **long-lived**: connect once and keep polling. Connecting is
187
+ the expensive, failure-prone part.
188
+ - **Not safe for concurrent use.** One request may be in flight at a time -
189
+ serialise with a lock if several tasks share a charger.
190
+ - Pass `on_disconnect=` to be told the moment the link drops rather than at your
191
+ next poll; check `charger.connected` before using it.
192
+ - The charger has a single connection slot. Always `disconnect()` when done.
193
+
194
+ ```python
195
+ charger = EaseeCharger(device, pin="1234", serial="EMX00000",
196
+ on_disconnect=lambda c: print("link lost"))
197
+ ```
198
+
199
+ ## Without bleak
200
+
201
+ `Session` is sans-io: it builds requests and parses replies and does no I/O, so
202
+ you can drive it over any transport.
203
+
204
+ ```python
205
+ from easee_ble import Session
206
+
207
+ s = Session(pin="1234", serial="EMX00000")
208
+ req = s.start_handshake() # write req.data to req.channel, feed the reply back
209
+ req = s.read_round_one(reply)
210
+ s.read_round_two(reply) # s.established is now True
211
+ frame = s.parse(req.channel, reply)
212
+ ```
213
+
214
+ ## Development
215
+
216
+ ```bash
217
+ pip install -e ".[dev]"
218
+ pytest
219
+ ```
220
+
221
+ Tests run against captured bytes from a real charger, so they need no hardware.
222
+
223
+ ## Notes
224
+
225
+ **Unofficial.** Reverse-engineered. Not affiliated with or endorsed by Easee. No warranty, changing charger settings is at your own risk. Barely tested.
226
+
@@ -0,0 +1,194 @@
1
+ # easee-ble
2
+
3
+ Local **Bluetooth** control for [Easee](https://easee.com) EV chargers. Talk to the charger directly with the PIN printed on the unit.
4
+
5
+ ```bash
6
+ pip install easee-ble
7
+ ```
8
+
9
+ Requires Python 3.11+. For Home Assistant, use
10
+ [ha-easee-ble](https://github.com/parrel/ha-easee-ble), which is built on this.
11
+
12
+ ## Quick start
13
+
14
+ ```python
15
+ import asyncio
16
+ from bleak import BleakScanner
17
+ from easee_ble import EaseeCharger, PhaseMode
18
+
19
+ async def main():
20
+ device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
21
+ charger = EaseeCharger(device, pin="1234", serial="EMX00000")
22
+
23
+ await charger.connect() # handshake; the link is held open
24
+ print(await charger.poll()) # {'chargerOpMode': 3, 'totalPower': 2.69, ...}
25
+
26
+ await charger.perform(lambda s: s.set_charger_enabled(True))
27
+ await charger.perform(lambda s: s.set_phase_mode(PhaseMode.AUTO))
28
+
29
+ await charger.disconnect()
30
+
31
+ asyncio.run(main())
32
+ ```
33
+
34
+ You need two things from the charger: the **PIN** printed on the unit, and its
35
+ **serial** (`EMXXXXXX`, also on the unit and in the Easee app).
36
+
37
+ ### Finding your charger
38
+
39
+ Scan by service UUID if you do not know the address:
40
+
41
+ ```python
42
+ from easee_ble import SERVICE_UUID
43
+
44
+ device = await BleakScanner.find_device_by_filter(
45
+ lambda d, adv: SERVICE_UUID in adv.service_uuids
46
+ )
47
+ ```
48
+
49
+ Chargers usually advertise only **intermittently**. To get one to show up,
50
+ either set Bluetooth to always-on in the Easee app, or open a window with a long
51
+ press of the charger's touch button.
52
+
53
+ ## Reading
54
+
55
+ `poll()` merges State and Config into one dict of named fields:
56
+
57
+ ```python
58
+ data = await charger.poll()
59
+ data["chargerOpMode"] # 3
60
+ data["totalPower"] # 2.69
61
+ data["maxChargerCurrent"] # 16
62
+ charger.unknown # fields we have no name for yet, by number
63
+ ```
64
+
65
+ Two fields belong together. `chargerOpMode` describes the **car and cable** - it
66
+ still reads `AWAITING_START` for a charger that has been switched off.
67
+ `reasonForNoCurrent` says **why** no current flows, and reads 53
68
+ (*charger disabled*) in exactly that case. Show both:
69
+
70
+ ```python
71
+ from easee_ble import charger_op_mode, reason_for_no_current
72
+
73
+ charger_op_mode(data["chargerOpMode"]) # ChargerOpMode.CHARGING
74
+ reason_for_no_current(data.get("reasonForNoCurrent")) # 'Charger disabled'
75
+ ```
76
+
77
+ Both return `None` on a value this library has not seen, where `ChargerOpMode()`
78
+ and `NetworkStatus()` raise. The descriptions are display text; key on
79
+ `reason_for_no_current_slug()` if you need something stable.
80
+
81
+ Fields at their default value are **absent** from the wire, not zero. `poll()`
82
+ already fills a curated set back in as `0`, so treat a missing field as unknown
83
+ rather than surprising.
84
+
85
+ ## Commands
86
+
87
+ Every command goes through `perform()`, which builds the request and checks the
88
+ answer:
89
+
90
+ ```python
91
+ await charger.perform(lambda s: s.set_charger_enabled(True))
92
+ await charger.perform(lambda s: s.set_max_charger_current(16))
93
+ await charger.perform(lambda s: s.set_dynamic_charger_current(10))
94
+ await charger.perform(lambda s: s.set_circuit_max_current(20)) # or (p1, p2, p3)
95
+ await charger.perform(lambda s: s.set_offline_max_circuit_current(10))
96
+ await charger.perform(lambda s: s.set_phase_mode(PhaseMode.LOCKED_3_PHASE))
97
+ await charger.perform(lambda s: s.set_led_brightness(75)) # 0-100
98
+ await charger.perform(lambda s: s.set_cable_locked(True))
99
+ await charger.perform(lambda s: s.set_access_control(True))
100
+ ```
101
+
102
+ `PhaseMode` is `LOCKED_1_PHASE`, `AUTO` or `LOCKED_3_PHASE`.
103
+
104
+ ### RFID / account keys
105
+
106
+ ```python
107
+ from easee_ble import command_payload
108
+
109
+ reply = await charger.perform(lambda s: s.list_user_tokens())
110
+ keys = command_payload(reply)
111
+ await charger.perform(lambda s: s.get_user_token(slot, name))
112
+ await charger.perform(lambda s: s.set_user_token(slot, name, token))
113
+ ```
114
+
115
+ ### Other reads
116
+
117
+ Besides `poll()`, individual frames are available through `perform()`:
118
+
119
+ ```python
120
+ await charger.perform(lambda s: s.poll_state())
121
+ await charger.perform(lambda s: s.poll_config())
122
+ await charger.perform(lambda s: s.poll_structure())
123
+ await charger.perform(lambda s: s.poll_debug())
124
+ ```
125
+
126
+ A reply only arrives on a subscribed channel, and the last two are not
127
+ subscribed to by default. Ask for them at connect - `EaseeCharger(...,
128
+ channels=DEFAULT_CHANNELS | {Channel.DEBUG})` - or the call raises. `poll()`
129
+ takes the same argument to read them alongside Config and State.
130
+
131
+ ## Errors
132
+
133
+ ```python
134
+ from easee_ble import EaseeCommandRefused, EaseeConnectionError, JPakeError
135
+
136
+ try:
137
+ await charger.connect()
138
+ await charger.perform(lambda s: s.set_max_charger_current(32))
139
+ except JPakeError: # almost always the wrong PIN
140
+ ...
141
+ except EaseeCommandRefused: # the charger answered, and said no
142
+ ...
143
+ except EaseeConnectionError: # connecting or talking to it failed
144
+ ...
145
+ ```
146
+
147
+ A reply is **not** an acknowledgement - the charger answers a command it refuses
148
+ just as promptly as one it accepts. `perform()` checks for you and raises
149
+ `EaseeCommandRefused`; anything else, `command_accepted()` and
150
+ `command_refusal()` check by hand.
151
+
152
+ ## Connection notes
153
+
154
+ - The connection is **long-lived**: connect once and keep polling. Connecting is
155
+ the expensive, failure-prone part.
156
+ - **Not safe for concurrent use.** One request may be in flight at a time -
157
+ serialise with a lock if several tasks share a charger.
158
+ - Pass `on_disconnect=` to be told the moment the link drops rather than at your
159
+ next poll; check `charger.connected` before using it.
160
+ - The charger has a single connection slot. Always `disconnect()` when done.
161
+
162
+ ```python
163
+ charger = EaseeCharger(device, pin="1234", serial="EMX00000",
164
+ on_disconnect=lambda c: print("link lost"))
165
+ ```
166
+
167
+ ## Without bleak
168
+
169
+ `Session` is sans-io: it builds requests and parses replies and does no I/O, so
170
+ you can drive it over any transport.
171
+
172
+ ```python
173
+ from easee_ble import Session
174
+
175
+ s = Session(pin="1234", serial="EMX00000")
176
+ req = s.start_handshake() # write req.data to req.channel, feed the reply back
177
+ req = s.read_round_one(reply)
178
+ s.read_round_two(reply) # s.established is now True
179
+ frame = s.parse(req.channel, reply)
180
+ ```
181
+
182
+ ## Development
183
+
184
+ ```bash
185
+ pip install -e ".[dev]"
186
+ pytest
187
+ ```
188
+
189
+ Tests run against captured bytes from a real charger, so they need no hardware.
190
+
191
+ ## Notes
192
+
193
+ **Unofficial.** Reverse-engineered. Not affiliated with or endorsed by Easee. No warranty, changing charger settings is at your own risk. Barely tested.
194
+
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "easee-ble"
7
+ dynamic = ["version"]
8
+ description = "Local Bluetooth control for Easee EV chargers"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "parrel" }]
13
+ keywords = ["easee", "ev", "charger", "bluetooth", "ble", "home-assistant"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Framework :: AsyncIO",
22
+ "Topic :: Home Automation",
23
+ ]
24
+ dependencies = [
25
+ # A 4-byte GCM nonce is legal but `cryptography` refuses any IV under 8
26
+ # bytes, so this cannot be swapped for it. See easee_ble/crypto.py.
27
+ "pycryptodome>=3.20",
28
+ # EC-JPAKE needs raw P-256 point arithmetic, which `cryptography` does not
29
+ # expose. Note ecdsa's own README: it is not side-channel hardened.
30
+ "ecdsa>=0.19",
31
+ "bleak>=0.22",
32
+ "bleak-retry-connector>=3.5.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Repository = "https://github.com/parrel/easee-ble"
37
+ "Bug Tracker" = "https://github.com/parrel/easee-ble/issues"
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=8",
42
+ "pytest-asyncio>=0.23",
43
+ "pytest-cov>=5",
44
+ "ruff>=0.6",
45
+ "mypy>=1.11",
46
+ ]
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "easee_ble.__version__" }
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.setuptools.package-data]
55
+ easee_ble = ["py.typed"]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
59
+ # The client tests are async; without this every one of them needs a decorator.
60
+ asyncio_mode = "auto"
61
+ asyncio_default_fixture_loop_scope = "function"
62
+
63
+ # `py.typed` promises callers that these annotations are checked, so check them.
64
+ [tool.mypy]
65
+ python_version = "3.11"
66
+ mypy_path = "src"
67
+ packages = ["easee_ble"]
68
+ strict = true
69
+ # The library is annotated; its dependencies are not all shipped with types.
70
+ [[tool.mypy.overrides]]
71
+ module = ["Crypto.*", "ecdsa.*", "bleak_retry_connector.*"]
72
+ ignore_missing_imports = true
73
+
74
+ [tool.ruff]
75
+ line-length = 100
76
+ target-version = "py311"
77
+
78
+ # Pinned explicitly rather than left to ruff's defaults, which shift between
79
+ # releases - CI should not start failing because a new ruff was published.
80
+ [tool.ruff.lint]
81
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
82
+ ignore = [
83
+ # __all__ is grouped by role, with comments. Alphabetising it would destroy
84
+ # the only structure it has.
85
+ "RUF022",
86
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,107 @@
1
+ """Local BLE control for Easee EV chargers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from . import commands, crypto, frames, protocol
6
+ from .client import DEFAULT_CHANNELS, DEFAULT_POLL_CHANNELS, EaseeCharger
7
+ from .commands import BtEnableMode, Command, NetworkStatus, PhaseMode, network_status
8
+ from .const import HANDSHAKE_MTU
9
+ from .crypto import decrypt, encrypt
10
+ from .exceptions import (
11
+ EaseeCommandRefused,
12
+ EaseeConnectionError,
13
+ EaseeError,
14
+ FrameError,
15
+ IncompleteFrame,
16
+ JPakeError,
17
+ ProtocolError,
18
+ RecordError,
19
+ SessionError,
20
+ )
21
+ from .frames import (
22
+ CONFIRMED_REASON_CODES,
23
+ FIELD_ALIASES,
24
+ FIELD_UNITS,
25
+ REASON_FOR_NO_CURRENT,
26
+ REASON_FOR_NO_CURRENT_SLUGS,
27
+ ZERO_WHEN_ABSENT,
28
+ ChargerOpMode,
29
+ Frame,
30
+ MessageType,
31
+ charger_op_mode,
32
+ command_accepted,
33
+ command_payload,
34
+ command_refusal,
35
+ declared_length,
36
+ parse_command_response,
37
+ parse_response,
38
+ reason_for_no_current,
39
+ reason_for_no_current_slug,
40
+ with_aliases,
41
+ )
42
+ from .jpake import ECJPake
43
+ from .protocol import CHANNEL_BY_UUID, MANUFACTURER_ID, SERVICE_UUID, Channel
44
+ from .session import Request, Session
45
+
46
+ __all__ = [
47
+ # client
48
+ "EaseeCharger",
49
+ # sans-io core
50
+ "Session",
51
+ "Request",
52
+ "ECJPake",
53
+ "encrypt",
54
+ "decrypt",
55
+ # commands and their value types
56
+ "Command",
57
+ "PhaseMode",
58
+ "BtEnableMode",
59
+ "NetworkStatus",
60
+ "network_status",
61
+ # responses
62
+ "Frame",
63
+ "MessageType",
64
+ "ChargerOpMode",
65
+ "charger_op_mode",
66
+ "REASON_FOR_NO_CURRENT",
67
+ "REASON_FOR_NO_CURRENT_SLUGS",
68
+ "CONFIRMED_REASON_CODES",
69
+ "FIELD_UNITS",
70
+ "reason_for_no_current",
71
+ "reason_for_no_current_slug",
72
+ "ZERO_WHEN_ABSENT",
73
+ # renaming a shipped field name; see "Field name stability" in the README
74
+ "FIELD_ALIASES",
75
+ "with_aliases",
76
+ "declared_length",
77
+ "parse_response",
78
+ "parse_command_response",
79
+ "command_accepted",
80
+ "command_payload",
81
+ "command_refusal",
82
+ # transport constants worth knowing about
83
+ "SERVICE_UUID",
84
+ "MANUFACTURER_ID",
85
+ "CHANNEL_BY_UUID",
86
+ "Channel",
87
+ "HANDSHAKE_MTU",
88
+ "DEFAULT_CHANNELS",
89
+ "DEFAULT_POLL_CHANNELS",
90
+ # exceptions
91
+ "EaseeError",
92
+ "EaseeConnectionError",
93
+ "EaseeCommandRefused",
94
+ "ProtocolError",
95
+ "FrameError",
96
+ "IncompleteFrame",
97
+ "RecordError",
98
+ "SessionError",
99
+ "JPakeError",
100
+ # submodules
101
+ "commands",
102
+ "crypto",
103
+ "frames",
104
+ "protocol",
105
+ ]
106
+
107
+ __version__ = "0.1.0"