Skip to main content

Control Mira Mode digital showers and bath fillers over Bluetooth LE

Project description

mira-mode-control

Lint

Control Mira Mode digital showers and bath fillers from Python, over Bluetooth Low Energy.

Mira Mode valves (now sold under Kohler) ship with no interface other than BLE and the vendor's phone app, which means no Home Assistant, no automation, and no way to start filling the bath from anywhere but the bathroom. This package implements the valve's BLE protocol directly, so you can drive it from a Raspberry Pi, a laptop, or anything else with a Bluetooth radio.

Use at your own risk. This drives real plumbing that produces real hot water. The protocol was reverse engineered, not documented by the vendor. Don't run it unattended until you've watched it behave.

Supported hardware

This implements the protocol used by current-generation valves, which advertise the GATT service 267f0001-eb15-43f5-94c3-67d2221188f7. Check yours with miramode info — it reports whether the valve speaks a protocol this package understands.

Earlier Mira Mode valves advertise bccb0001-ca66-11e5-88a4-0002a5d5c51b and use an unrelated, CRC-16 based protocol that this package does not support.

Developed against a dual outlet shower and bath filler unit. Other valves in the range should work, but only the outlets and presets your unit actually has will do anything.

Requirements

  • Python 3.10 or newer
  • A Bluetooth Low Energy adapter
  • On Linux, BlueZ
  • The valve must be paired with the host first, through your operating system's Bluetooth settings. The valve refuses to accept commands over an unbonded connection.

Installation

pip install miramode

Or, with uvuv tool install miramode to get just the miramode command, or uv add miramode to use it as a library in a project.

From source

git clone git@github.com:ryan-shaw/mira-mode-control.git
cd mira-mode-control
uv sync

That installs the versions pinned in uv.lock into .venv. Prefix the commands below with uv run to use it.

Development

The same checks CI runs:

uv run ruff check .
uv run ruff format --check .
uv run ty check

Use uv run ruff format . to apply formatting.

Command line usage

Find your valve:

$ miramode scan
1FE6A4BD-73A5-055E-6463-F4785E21D49D  Mira 004C Bathroom

Addresses are MAC addresses on Linux and Windows, and opaque UUIDs on macOS. Every command below takes one with -a.

Status

To see what the valve is doing right now, without changing anything:

$ miramode status -a <address>
Running:      yes, outlet 1
Temperature:  38.4C
Target:       41.0C
Flow:         75%

When nothing is running, only the measured temperature is shown, since the valve reports no target and a resting flow value while idle. Add -v to see the undecoded reply alongside it.

Presets

Presets are the programmes stored in the valve — each carries its own target temperature and run time, and they're how the vendor app starts the shower or fills the bath. Listing them only reads from the valve and runs no water:

$ miramode presets -a <address>
  1  Default
  2  Default Bathfill

Factory-fitted presets are numbered from 1; slot 0 is not used. Start one by number:

miramode start -a <address> 2

Outlets

To run an outlet directly, at a temperature you choose:

miramode outlets -a <address> --first --temperature 39

Use --second for the second outlet, and both flags together to run both at once. A command states the complete desired state, so any outlet you don't name is switched off. --flow sets the flow rate as a percentage, defaulting to 100.

Which physical fixture is "first" depends on how your unit is plumbed.

To shut everything off, including a running preset:

miramode stop -a <address>

Troubleshooting

-v logs every frame exchanged with the valve:

$ miramode -v stop -a <address>
Connected to 1FE6A4BD-73A5-055E-6463-F4785E21D49D
Sending aa 55 00 ab 04 00 00 00 00 52
Received channel=1 opcode=0x01 payload=01

Library usage

The API is async, built on bleak:

import asyncio
from miramode import Outlet, Shower


async def run_a_bath():
    async with Shower("1FE6A4BD-73A5-055E-6463-F4785E21D49D") as shower:
        for preset in await shower.presets():
            print(preset.index, preset.name)

        await shower.run_preset(2)  # start the bath filling
        await asyncio.sleep(300)
        await shower.stop()

        # or drive an outlet directly
        await shower.set_outlets(Outlet.FIRST, temperature=39.0)


asyncio.run(run_a_bath())

Failures raise subclasses of MiraError: NotConnected, ResponseTimeout, and CommandFailed when the valve rejects a command.

Protocol notes

Messages in both directions share one frame layout:

aa 55 <channel> <opcode> <length> <payload...> <checksum>

The checksum is the two's complement of the sum of the preceding bytes, so a whole valid frame sums to zero modulo 256. Commands are written to characteristic 267f0002-… and replies arrive as notifications on 267f0003-…, one reply per command.

Four opcodes are implemented:

Opcode Meaning Payload
0xab Set outlets temperature (2 bytes, tenths of a degree, big endian), flow percentage, outlet bitfield
0xb1 Run preset preset slot
0x5d Read preset preset slot; the reply echoes the slot then a 16 byte NUL-padded name
0x2b Read state a constant 0x02; the reply is described below

The state reply carries more than is decoded here. These offsets into its payload were confirmed by commanding known values and reading them back, and everything else is left in State.raw:

Bytes Meaning
10-11 target temperature, tenths of a degree, big endian
12 flow percentage
13 outlet bitfield, same layout as the one sent to 0xab
14-15 measured water temperature, tenths of a degree, big endian

Note that replies to 0x2b use the same 0x01 opcode as an ordinary acknowledgement, just with a long payload, so a reply cannot be identified by its opcode alone.

The outlet field is a bitfield in a single byte — bit 0 is the first outlet, bit 1 the second, bit 2 a third — rather than one byte per outlet. A temperature of zero means "leave the setting alone", which is what stop sends.

Acknowledgements

Nigel Hannam's protocol documentation was a useful reference for the older generation of these valves.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

miramode-1.1.0.tar.gz (41.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

miramode-1.1.0-py3-none-any.whl (14.1 kB view details)

Uploaded Python 3

File details

Details for the file miramode-1.1.0.tar.gz.

File metadata

  • Download URL: miramode-1.1.0.tar.gz
  • Upload date:
  • Size: 41.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for miramode-1.1.0.tar.gz
Algorithm Hash digest
SHA256 6c924b209a29d74d05d999264950687d0d4be12f22ab0abda27df5c536f24bff
MD5 bd549fec3ff823f2383da735c525d679
BLAKE2b-256 55dde51a4e03bd22817b24ce5cd124559bafb566410b3ac35b83025c13ce0b56

See more details on using hashes here.

File details

Details for the file miramode-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: miramode-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for miramode-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c3d4b534b95cbc10edfaaa05a331a17d6470b9659a805e4c54466056792821f1
MD5 c5f8112b0350a0da0899ea915866007b
BLAKE2b-256 78d092ed8f4fb1b95f8f7936d7d00dc0e5fd7ce331b3fac936798a24add3a4fb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page