miramode 1.0.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,2 @@
1
+ # Always use LF
2
+ * text=lf
@@ -0,0 +1,59 @@
1
+ name: Lint
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ # A new push to the same branch supersedes any run still in progress.
13
+ concurrency:
14
+ group: ${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ ruff:
19
+ name: ruff
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7.0.1
23
+
24
+ # Pinned to an exact release: setup-uv publishes no moving v9 major
25
+ # tag, so `@v9` does not resolve.
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v9.0.0
28
+ with:
29
+ enable-cache: true
30
+
31
+ # --locked fails if uv.lock has drifted from pyproject.toml, so CI
32
+ # checks the same versions that are pinned for everyone else.
33
+ - name: Install dependencies
34
+ run: uv sync --locked
35
+
36
+ - name: Run ruff
37
+ run: uv run ruff check --output-format=github .
38
+
39
+ - name: Check formatting
40
+ run: uv run ruff format --check .
41
+
42
+ ty:
43
+ name: ty
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v7.0.1
47
+
48
+ # Pinned to an exact release: setup-uv publishes no moving v9 major
49
+ # tag, so `@v9` does not resolve.
50
+ - name: Install uv
51
+ uses: astral-sh/setup-uv@v9.0.0
52
+ with:
53
+ enable-cache: true
54
+
55
+ - name: Install dependencies
56
+ run: uv sync --locked
57
+
58
+ - name: Run ty
59
+ run: uv run ty check --output-format=github
@@ -0,0 +1,47 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ name: Build and publish to PyPI
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ # Required for PyPI trusted publishing: the job exchanges a short
16
+ # lived OIDC token for an upload token, so no API token is stored.
17
+ id-token: write
18
+ steps:
19
+ - uses: actions/checkout@v7.0.1
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v9.0.0
23
+ with:
24
+ enable-cache: true
25
+
26
+ # A release tagged v1.2.3 must contain version 1.2.3, or the upload
27
+ # silently publishes something other than what the tag claims.
28
+ - name: Check tag matches package version
29
+ run: |
30
+ # --color never: uv colourises even when piped, and the escape
31
+ # codes would make this comparison fail for every tag.
32
+ version=$(uv version --short --color never)
33
+ tag="${GITHUB_REF_NAME#v}"
34
+ if [ "$version" != "$tag" ]; then
35
+ echo "::error::Tag $GITHUB_REF_NAME does not match project version $version"
36
+ exit 1
37
+ fi
38
+ echo "Publishing version $version"
39
+
40
+ - name: Build
41
+ run: uv build
42
+
43
+ - name: Check metadata
44
+ run: uvx twine check dist/*
45
+
46
+ - name: Publish
47
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,15 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Environments
9
+ .venv/
10
+ venv/
11
+
12
+ # Tooling caches
13
+ .ruff_cache/
14
+ .ty_cache/
15
+ .pytest_cache/
miramode-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Shaw
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,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: miramode
3
+ Version: 1.0.0
4
+ Summary: Control Mira Mode digital showers and bath fillers over Bluetooth LE
5
+ Project-URL: Homepage, https://github.com/ryan-shaw/mira-mode-control
6
+ Project-URL: Repository, https://github.com/ryan-shaw/mira-mode-control
7
+ Project-URL: Issues, https://github.com/ryan-shaw/mira-mode-control/issues
8
+ Author: Ryan Shaw
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ble,bluetooth,home-automation,kohler,mira,mira-mode,shower,smart-home
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Home Automation
24
+ Classifier: Topic :: System :: Hardware
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: bleak>=0.22
28
+ Description-Content-Type: text/markdown
29
+
30
+ # mira-mode-control
31
+
32
+ [![Lint](https://github.com/ryan-shaw/mira-mode-control/actions/workflows/lint.yml/badge.svg)](https://github.com/ryan-shaw/mira-mode-control/actions/workflows/lint.yml)
33
+
34
+ Control Mira Mode digital showers and bath fillers from Python, over
35
+ Bluetooth Low Energy.
36
+
37
+ Mira Mode valves (now sold under Kohler) ship with no interface other than
38
+ BLE and the vendor's phone app, which means no Home Assistant, no
39
+ automation, and no way to start filling the bath from anywhere but the
40
+ bathroom. This package implements the valve's BLE protocol directly, so
41
+ you can drive it from a Raspberry Pi, a laptop, or anything else with a
42
+ Bluetooth radio.
43
+
44
+ > **Use at your own risk.** This drives real plumbing that produces real
45
+ > hot water. The protocol was reverse engineered, not documented by the
46
+ > vendor. Don't run it unattended until you've watched it behave.
47
+
48
+ ## Supported hardware
49
+
50
+ This implements the protocol used by current-generation valves, which
51
+ advertise the GATT service `267f0001-eb15-43f5-94c3-67d2221188f7`. Check
52
+ yours with `miramode info` — it reports whether the valve speaks a
53
+ protocol this package understands.
54
+
55
+ Earlier Mira Mode valves advertise `bccb0001-ca66-11e5-88a4-0002a5d5c51b`
56
+ and use an unrelated, CRC-16 based protocol that this package does not
57
+ support.
58
+
59
+ Developed against a dual outlet shower and bath filler unit. Other valves
60
+ in the range should work, but only the outlets and presets your unit
61
+ actually has will do anything.
62
+
63
+ ## Requirements
64
+
65
+ - Python 3.10 or newer
66
+ - A Bluetooth Low Energy adapter
67
+ - On Linux, BlueZ
68
+ - The valve must be paired with the host first, through your operating
69
+ system's Bluetooth settings. The valve refuses to accept commands over
70
+ an unbonded connection.
71
+
72
+ ## Installation
73
+
74
+ ```console
75
+ pip install miramode
76
+ ```
77
+
78
+ Or, with [uv](https://docs.astral.sh/uv/) — `uv tool install miramode`
79
+ to get just the `miramode` command, or `uv add miramode` to use it as a
80
+ library in a project.
81
+
82
+ ### From source
83
+
84
+ ```console
85
+ git clone git@github.com:ryan-shaw/mira-mode-control.git
86
+ cd mira-mode-control
87
+ uv sync
88
+ ```
89
+
90
+ That installs the versions pinned in `uv.lock` into `.venv`. Prefix the
91
+ commands below with `uv run` to use it.
92
+
93
+ ### Development
94
+
95
+ The same checks CI runs:
96
+
97
+ ```console
98
+ uv run ruff check .
99
+ uv run ruff format --check .
100
+ uv run ty check
101
+ ```
102
+
103
+ Use `uv run ruff format .` to apply formatting.
104
+
105
+ ## Command line usage
106
+
107
+ Find your valve:
108
+
109
+ ```console
110
+ $ miramode scan
111
+ 1FE6A4BD-73A5-055E-6463-F4785E21D49D Mira 004C Bathroom
112
+ ```
113
+
114
+ Addresses are MAC addresses on Linux and Windows, and opaque UUIDs on
115
+ macOS. Every command below takes one with `-a`.
116
+
117
+ ### Presets
118
+
119
+ Presets are the programmes stored in the valve — each carries its own
120
+ target temperature and run time, and they're how the vendor app starts
121
+ the shower or fills the bath. Listing them only reads from the valve and
122
+ runs no water:
123
+
124
+ ```console
125
+ $ miramode presets -a <address>
126
+ 1 Default
127
+ 2 Default Bathfill
128
+ ```
129
+
130
+ Factory-fitted presets are numbered from 1; slot 0 is not used. Start one
131
+ by number:
132
+
133
+ ```console
134
+ miramode start -a <address> 2
135
+ ```
136
+
137
+ ### Outlets
138
+
139
+ To run an outlet directly, at a temperature you choose:
140
+
141
+ ```console
142
+ miramode outlets -a <address> --first --temperature 39
143
+ ```
144
+
145
+ Use `--second` for the second outlet, and both flags together to run both
146
+ at once. A command states the complete desired state, so any outlet you
147
+ don't name is switched off. `--flow` sets the flow rate as a percentage,
148
+ defaulting to 100.
149
+
150
+ Which physical fixture is "first" depends on how your unit is plumbed.
151
+
152
+ To shut everything off, including a running preset:
153
+
154
+ ```console
155
+ miramode stop -a <address>
156
+ ```
157
+
158
+ ### Troubleshooting
159
+
160
+ `-v` logs every frame exchanged with the valve:
161
+
162
+ ```console
163
+ $ miramode -v stop -a <address>
164
+ Connected to 1FE6A4BD-73A5-055E-6463-F4785E21D49D
165
+ Sending aa 55 00 ab 04 00 00 00 00 52
166
+ Received channel=1 opcode=0x01 payload=01
167
+ ```
168
+
169
+ ## Library usage
170
+
171
+ The API is async, built on [bleak](https://github.com/hbldh/bleak):
172
+
173
+ ```python
174
+ import asyncio
175
+ from miramode import Outlet, Shower
176
+
177
+
178
+ async def run_a_bath():
179
+ async with Shower("1FE6A4BD-73A5-055E-6463-F4785E21D49D") as shower:
180
+ for preset in await shower.presets():
181
+ print(preset.index, preset.name)
182
+
183
+ await shower.run_preset(2) # start the bath filling
184
+ await asyncio.sleep(300)
185
+ await shower.stop()
186
+
187
+ # or drive an outlet directly
188
+ await shower.set_outlets(Outlet.FIRST, temperature=39.0)
189
+
190
+
191
+ asyncio.run(run_a_bath())
192
+ ```
193
+
194
+ Failures raise subclasses of `MiraError`: `NotConnected`,
195
+ `ResponseTimeout`, and `CommandFailed` when the valve rejects a command.
196
+
197
+ ## Protocol notes
198
+
199
+ Messages in both directions share one frame layout:
200
+
201
+ ```
202
+ aa 55 <channel> <opcode> <length> <payload...> <checksum>
203
+ ```
204
+
205
+ The checksum is the two's complement of the sum of the preceding bytes,
206
+ so a whole valid frame sums to zero modulo 256. Commands are written to
207
+ characteristic `267f0002-…` and replies arrive as notifications on
208
+ `267f0003-…`, one reply per command.
209
+
210
+ Three opcodes are implemented:
211
+
212
+ | Opcode | Meaning | Payload |
213
+ | --- | --- | --- |
214
+ | `0xab` | Set outlets | temperature (2 bytes, tenths of a degree, big endian), flow percentage, outlet bitfield |
215
+ | `0xb1` | Run preset | preset slot |
216
+ | `0x5d` | Read preset | preset slot; the reply echoes the slot then a 16 byte NUL-padded name |
217
+
218
+ The outlet field is a bitfield in a single byte — bit 0 is the first
219
+ outlet, bit 1 the second, bit 2 a third — rather than one byte per
220
+ outlet. A temperature of zero means "leave the setting alone", which is
221
+ what `stop` sends.
222
+
223
+ ## Acknowledgements
224
+
225
+ Nigel Hannam's [protocol
226
+ documentation](https://github.com/nhannam/shower-controller-documentation)
227
+ was a useful reference for the older generation of these valves.
228
+
229
+ ## License
230
+
231
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,202 @@
1
+ # mira-mode-control
2
+
3
+ [![Lint](https://github.com/ryan-shaw/mira-mode-control/actions/workflows/lint.yml/badge.svg)](https://github.com/ryan-shaw/mira-mode-control/actions/workflows/lint.yml)
4
+
5
+ Control Mira Mode digital showers and bath fillers from Python, over
6
+ Bluetooth Low Energy.
7
+
8
+ Mira Mode valves (now sold under Kohler) ship with no interface other than
9
+ BLE and the vendor's phone app, which means no Home Assistant, no
10
+ automation, and no way to start filling the bath from anywhere but the
11
+ bathroom. This package implements the valve's BLE protocol directly, so
12
+ you can drive it from a Raspberry Pi, a laptop, or anything else with a
13
+ Bluetooth radio.
14
+
15
+ > **Use at your own risk.** This drives real plumbing that produces real
16
+ > hot water. The protocol was reverse engineered, not documented by the
17
+ > vendor. Don't run it unattended until you've watched it behave.
18
+
19
+ ## Supported hardware
20
+
21
+ This implements the protocol used by current-generation valves, which
22
+ advertise the GATT service `267f0001-eb15-43f5-94c3-67d2221188f7`. Check
23
+ yours with `miramode info` — it reports whether the valve speaks a
24
+ protocol this package understands.
25
+
26
+ Earlier Mira Mode valves advertise `bccb0001-ca66-11e5-88a4-0002a5d5c51b`
27
+ and use an unrelated, CRC-16 based protocol that this package does not
28
+ support.
29
+
30
+ Developed against a dual outlet shower and bath filler unit. Other valves
31
+ in the range should work, but only the outlets and presets your unit
32
+ actually has will do anything.
33
+
34
+ ## Requirements
35
+
36
+ - Python 3.10 or newer
37
+ - A Bluetooth Low Energy adapter
38
+ - On Linux, BlueZ
39
+ - The valve must be paired with the host first, through your operating
40
+ system's Bluetooth settings. The valve refuses to accept commands over
41
+ an unbonded connection.
42
+
43
+ ## Installation
44
+
45
+ ```console
46
+ pip install miramode
47
+ ```
48
+
49
+ Or, with [uv](https://docs.astral.sh/uv/) — `uv tool install miramode`
50
+ to get just the `miramode` command, or `uv add miramode` to use it as a
51
+ library in a project.
52
+
53
+ ### From source
54
+
55
+ ```console
56
+ git clone git@github.com:ryan-shaw/mira-mode-control.git
57
+ cd mira-mode-control
58
+ uv sync
59
+ ```
60
+
61
+ That installs the versions pinned in `uv.lock` into `.venv`. Prefix the
62
+ commands below with `uv run` to use it.
63
+
64
+ ### Development
65
+
66
+ The same checks CI runs:
67
+
68
+ ```console
69
+ uv run ruff check .
70
+ uv run ruff format --check .
71
+ uv run ty check
72
+ ```
73
+
74
+ Use `uv run ruff format .` to apply formatting.
75
+
76
+ ## Command line usage
77
+
78
+ Find your valve:
79
+
80
+ ```console
81
+ $ miramode scan
82
+ 1FE6A4BD-73A5-055E-6463-F4785E21D49D Mira 004C Bathroom
83
+ ```
84
+
85
+ Addresses are MAC addresses on Linux and Windows, and opaque UUIDs on
86
+ macOS. Every command below takes one with `-a`.
87
+
88
+ ### Presets
89
+
90
+ Presets are the programmes stored in the valve — each carries its own
91
+ target temperature and run time, and they're how the vendor app starts
92
+ the shower or fills the bath. Listing them only reads from the valve and
93
+ runs no water:
94
+
95
+ ```console
96
+ $ miramode presets -a <address>
97
+ 1 Default
98
+ 2 Default Bathfill
99
+ ```
100
+
101
+ Factory-fitted presets are numbered from 1; slot 0 is not used. Start one
102
+ by number:
103
+
104
+ ```console
105
+ miramode start -a <address> 2
106
+ ```
107
+
108
+ ### Outlets
109
+
110
+ To run an outlet directly, at a temperature you choose:
111
+
112
+ ```console
113
+ miramode outlets -a <address> --first --temperature 39
114
+ ```
115
+
116
+ Use `--second` for the second outlet, and both flags together to run both
117
+ at once. A command states the complete desired state, so any outlet you
118
+ don't name is switched off. `--flow` sets the flow rate as a percentage,
119
+ defaulting to 100.
120
+
121
+ Which physical fixture is "first" depends on how your unit is plumbed.
122
+
123
+ To shut everything off, including a running preset:
124
+
125
+ ```console
126
+ miramode stop -a <address>
127
+ ```
128
+
129
+ ### Troubleshooting
130
+
131
+ `-v` logs every frame exchanged with the valve:
132
+
133
+ ```console
134
+ $ miramode -v stop -a <address>
135
+ Connected to 1FE6A4BD-73A5-055E-6463-F4785E21D49D
136
+ Sending aa 55 00 ab 04 00 00 00 00 52
137
+ Received channel=1 opcode=0x01 payload=01
138
+ ```
139
+
140
+ ## Library usage
141
+
142
+ The API is async, built on [bleak](https://github.com/hbldh/bleak):
143
+
144
+ ```python
145
+ import asyncio
146
+ from miramode import Outlet, Shower
147
+
148
+
149
+ async def run_a_bath():
150
+ async with Shower("1FE6A4BD-73A5-055E-6463-F4785E21D49D") as shower:
151
+ for preset in await shower.presets():
152
+ print(preset.index, preset.name)
153
+
154
+ await shower.run_preset(2) # start the bath filling
155
+ await asyncio.sleep(300)
156
+ await shower.stop()
157
+
158
+ # or drive an outlet directly
159
+ await shower.set_outlets(Outlet.FIRST, temperature=39.0)
160
+
161
+
162
+ asyncio.run(run_a_bath())
163
+ ```
164
+
165
+ Failures raise subclasses of `MiraError`: `NotConnected`,
166
+ `ResponseTimeout`, and `CommandFailed` when the valve rejects a command.
167
+
168
+ ## Protocol notes
169
+
170
+ Messages in both directions share one frame layout:
171
+
172
+ ```
173
+ aa 55 <channel> <opcode> <length> <payload...> <checksum>
174
+ ```
175
+
176
+ The checksum is the two's complement of the sum of the preceding bytes,
177
+ so a whole valid frame sums to zero modulo 256. Commands are written to
178
+ characteristic `267f0002-…` and replies arrive as notifications on
179
+ `267f0003-…`, one reply per command.
180
+
181
+ Three opcodes are implemented:
182
+
183
+ | Opcode | Meaning | Payload |
184
+ | --- | --- | --- |
185
+ | `0xab` | Set outlets | temperature (2 bytes, tenths of a degree, big endian), flow percentage, outlet bitfield |
186
+ | `0xb1` | Run preset | preset slot |
187
+ | `0x5d` | Read preset | preset slot; the reply echoes the slot then a 16 byte NUL-padded name |
188
+
189
+ The outlet field is a bitfield in a single byte — bit 0 is the first
190
+ outlet, bit 1 the second, bit 2 a third — rather than one byte per
191
+ outlet. A temperature of zero means "leave the setting alone", which is
192
+ what `stop` sends.
193
+
194
+ ## Acknowledgements
195
+
196
+ Nigel Hannam's [protocol
197
+ documentation](https://github.com/nhannam/shower-controller-documentation)
198
+ was a useful reference for the older generation of these valves.
199
+
200
+ ## License
201
+
202
+ MIT — see [LICENSE](LICENSE).