Metadata-Version: 2.5
Name: safera-sense-ble
Version: 0.3.0
Summary: BLE client library for Safera Sense cooking sensors (Røros Hetta cooker hoods, Safera stove guards)
Project-URL: Homepage, https://github.com/crillebaba/safera-sense-ble
Project-URL: Repository, https://github.com/crillebaba/safera-sense-ble
Project-URL: Issues, https://github.com/crillebaba/safera-sense-ble/issues
Author: Christophe Baraër
License-Expression: MIT
License-File: LICENSE
Keywords: ble,bluetooth,cooker-hood,hetta,roros,safera,stove-guard
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Requires-Python: >=3.11
Requires-Dist: bleak-retry-connector>=3.4.0
Requires-Dist: bleak>=0.21.0
Description-Content-Type: text/markdown

# safera-sense-ble

Async Python library for talking to **Safera Sense** cooking sensors over
Bluetooth Low Energy — as found in **Røros Hetta** cooker hoods
(Safera Sense Integral) and Safera stove guards.

[![CI](https://github.com/crillebaba/safera-sense-ble/actions/workflows/ci.yml/badge.svg)](https://github.com/crillebaba/safera-sense-ble/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/safera-sense-ble.svg)](https://pypi.org/project/safera-sense-ble/)

## Credits

Most of the protocol reverse engineering comes from two projects that
did the heavy lifting:

- **[magicus/safera-ble](https://github.com/magicus/safera-ble)** — the
  byte-level protocol documentation, derived from packet captures and
  analysis of the decompiled Android app;
- **[havardgulldahl/rorossense-ble](https://github.com/havardgulldahl/rorossense-ble)** —
  a working Python client and further protocol exploration against a
  RørosHetta hood, including the
  [consolidated protocol docs](https://github.com/havardgulldahl/rorossense-ble/blob/main/docs/safera-ble-protocol.md).

This library is an independent, from-scratch implementation of that
protocol, extended with findings made during its development (verified
on an `IFU10CR-PRO`, firmware 13/75):

- the proprietary characteristics require **BLE bonding** — the device
  answers ATT "Insufficient authentication" until paired; this library
  pairs on demand and retries;
- **grease filter saturation** lives at byte 59 of the extended sensor
  report and resets via the `SET_HOOD_FILTER_CHANGED` command
  (confirmed with a before/after diff around the vendor app's filter
  reset);
- the "particle index" field tracks the vendor app's **PM2.5** reading
  (µg/m³, 0.2 µg/m³ raw resolution).

Unofficial project — not affiliated with Safera Oy or Røros Metall AS.

## Features

- **Live sensor stream**: subscribe to notifications (~1 Hz) parsed into a
  typed `SensorReport` — ambient/surface/pan temperature, humidity,
  ambient light, eCO2, tVOC, air quality index, PM2.5, stove power
  draw, cooking activity, alarm level, grease filter saturation,
  device state and error bitfields.
- **Control**: hood fan speeds 1–4 (level 4 = boost), auto mode; light levels 1–3;
  identify; grease-filter reset.
- **Smart Cooking event log**: read or subscribe to the timeline of
  cooking events (cooking/frying/boiling/heating started, stove alarms,
  button presses); clear it too.
- **Device info**: model, serial, hardware/firmware revisions, Wi-Fi
  status (SSID, RSSI, device name).
- **Sensible precision**: values are quantized to each sensor's
  meaningful resolution (0.1 °C temperatures, whole-percent humidity,
  whole lux, 0.1 µg/m³ PM2.5) so consumers don't see measurement
  jitter as state changes.
- Built on [bleak](https://github.com/hbldh/bleak) and
  [bleak-retry-connector](https://github.com/Bluetooth-Devices/bleak-retry-connector);
  plays well with Home Assistant's Bluetooth stack but has **no Home
  Assistant dependency**.

## Installation

```bash
pip install safera-sense-ble
```

## Usage

### Find the device

Safera devices advertise the proprietary service UUID and, depending on
branding, a name like `Røroshetta`, `iSense…` or `Sense_…`:

```python
import asyncio
from bleak import BleakScanner
from safera_sense_ble import SAFERA_SERVICE_UUID

async def find():
    devices = await BleakScanner.discover(return_adv=True)
    for device, adv in devices.values():
        if SAFERA_SERVICE_UUID in adv.service_uuids:
            print(device.address, adv.local_name)

asyncio.run(find())
```

### Stream sensor data

```python
import asyncio
from bleak import BleakScanner
from safera_sense_ble import SaferaSenseClient, SensorReport

async def monitor(address: str):
    ble_device = await BleakScanner.find_device_by_address(address)
    client = SaferaSenseClient(ble_device)
    await client.connect()

    info = await client.fetch_device_info()
    print(f"Connected to {info.model} (fw {info.firmware_rev})")

    def on_report(report: SensorReport) -> None:
        print(
            f"{report.ambient_temperature:.1f} °C  "
            f"{report.humidity} %RH  "
            f"eCO2 {report.co2_ppm} ppm  "
            f"PM2.5 {report.particle_index} µg/m³  "
            f"filter {report.grease_filter} %"
        )

    # The device requires bonding for this; the client pairs on demand.
    await client.subscribe_sensor_reports(on_report)
    await asyncio.sleep(30)
    await client.disconnect()

asyncio.run(monitor("D4:6A:C8:XX:XX:XX"))
```

### Control the hood

```python
from safera_sense_ble import FanSpeed, LightLevel

await client.set_fan_speed(FanSpeed.LEVEL_2)   # speeds 1-4
await client.set_fan_speed(FanSpeed.BOOST)     # top step (level 4)
await client.set_fan_auto()                    # air-quality controlled
await client.set_fan_speed(FanSpeed.OFF)

await client.set_light_level(LightLevel.LEVEL_3)  # light levels 1-3
await client.set_light_level(LightLevel.OFF)
await client.toggle_light_auto()               # toggle presence-based auto

await client.identify()                        # make the device identify itself
await client.reset_grease_filter()             # after cleaning the filter
```

### One-shot reads

```python
report = await client.fetch_sensor_report()    # single parsed snapshot
wifi = await client.fetch_wifi_status()        # SSID, RSSI, device name, IP
```

### Smart Cooking event log

```python
from safera_sense_ble import CookingEvent

events = await client.fetch_event_log()        # current timeline (newest first)
for e in events:
    print(e.name, e.timestamp)                 # e.g. "frying_start", device clock

def on_events(events: list[CookingEvent]) -> None:
    if events:
        print("latest:", events[0].name)

await client.subscribe_event_log(on_events)    # notified on new events
await client.clear_event_log()                 # the app's "Clear Timeline"
```

Event timestamps are in device-clock seconds; correlate them with the
live `SensorReport.device_clock` if you need wall-clock times.

See [examples/monitor.py](examples/monitor.py) for a runnable script.

## API notes

- All methods are coroutines; the client is designed for a single
  long-lived connection (the device accepts one central at a time — close
  the vendor app while connected).
- `SensorReport.from_bytes` accepts both the documented 54-byte record
  and the extended (~69-byte) record sent by hood-integrated devices;
  hood-only fields (`fan_speed_raw`, `light_raw`, `grease_filter`, …)
  are `None` when absent.
- `SaferaSenseClient.dump_characteristics()` hex-dumps every readable
  characteristic — useful for decoding the remaining unknown fields
  (byte 58 of the sensor report, the event log, `READ_SETTINGS`, …);
  contributions welcome here and upstream.

## License

MIT — see [LICENSE](LICENSE).
