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.

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.

Three 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

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.0.0.tar.gz (40.2 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.0.0-py3-none-any.whl (12.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: miramode-1.0.0.tar.gz
  • Upload date:
  • Size: 40.2 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.0.0.tar.gz
Algorithm Hash digest
SHA256 3127e06a495ce6af891db1cac4cdb3e9cd38a9eaed826e4c210979a9d2cda119
MD5 ffb40c508f2e72558adc0ff4faa836c1
BLAKE2b-256 2c594011cfa6a81ccd32471f65bbfc8c743c24b5bf42782fe98bc6e47eed14e1

See more details on using hashes here.

File details

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

File metadata

  • Download URL: miramode-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 12.6 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.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 40cb306153fe89cae4f9d640e2c798d4a8dd61173f4ed87e8d48dadc41bb1eb6
MD5 20f3995f47e69c7db1153e15f1880dca
BLAKE2b-256 37db91a7707b66d85baa7ace441b2f481e1a9ab1a1270cdb95a2ff2809ee8367

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