Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bkbilly committed Mar 12, 2024
0 parents commit 056ac4d
Show file tree
Hide file tree
Showing 16 changed files with 657 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: home-assistant/actions/hassfest@master
18 changes: 18 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Validate

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
10 changes: 10 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
MIT License

Copyright (c) 2024 Vasilis Koulis

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[![GitHub Release](https://img.shields.io/github/release/bkbilly/medisanabp_ble.svg?style=flat-square)](https://github.com/bkbilly/medisanabp_ble/releases)
[![License](https://img.shields.io/github/license/bkbilly/medisanabp_ble.svg?style=flat-square)](LICENSE)
[![hacs](https://img.shields.io/badge/HACS-default-orange.svg?style=flat-square)](https://hacs.xyz)


# Medisana Blood Pressure BLE
Integrates Bluetooth LE (https://www.medisana.com/en/Health-control/Blood-pressure-monitor/) to Home Assistant using active connection to get infromation from the sensors.

Exposes the following sensors:
- Battery
- Diastolic pressure
- Systolic pressure
- Pulses
- Measured date

## Installation

Easiest install is via [HACS](https://hacs.xyz/):

[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=bkbilly&repository=medisanabp_ble&category=integration)

`HACS -> Explore & Add Repositories -> Medisana Blood Pressure BLE`

The device will be autodiscovered once the data are received by any bluetooth proxy.
96 changes: 96 additions & 0 deletions custom_components/medisanabp_ble/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""The MedisanaBP integration."""

from __future__ import annotations

import logging


from homeassistant.components.bluetooth import (
BluetoothScanningMode,
BluetoothServiceInfoBleak,
async_ble_device_from_address,
)
from homeassistant.components.bluetooth.active_update_processor import (
ActiveBluetoothProcessorCoordinator,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import CoreState, HomeAssistant

from .medisana_bp import MedisanaBPBluetoothDeviceData, SensorUpdate
from .const import DOMAIN

PLATFORMS: list[Platform] = [Platform.SENSOR]

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up MedisanaBP BLE device from a config entry."""
address = entry.unique_id
assert address is not None
data = MedisanaBPBluetoothDeviceData()

def _needs_poll(
service_info: BluetoothServiceInfoBleak, last_poll: float | None
) -> bool:
# Only poll if hass is running, we need to poll,
# and we actually have a way to connect to the device
return (
hass.state is CoreState.running
and data.poll_needed(service_info, last_poll)
and bool(
async_ble_device_from_address(
hass, service_info.device.address, connectable=True
)
)
)

async def _async_poll(service_info: BluetoothServiceInfoBleak) -> SensorUpdate:
# BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it
# directly to the elissabp code
# Make sure the device we have is one that we can connect with
# in case its coming from a passive scanner
if service_info.connectable:
connectable_device = service_info.device
elif device := async_ble_device_from_address(
hass, service_info.device.address, True
):
connectable_device = device
else:
# We have no bluetooth controller that is in range of
# the device to poll it
raise RuntimeError(
f"No connectable device found for {service_info.device.address}"
)
return await data.async_poll(connectable_device)

coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id
] = ActiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=data.update,
needs_poll_method=_needs_poll,
poll_method=_async_poll,
# We will take advertisements from non-connectable devices
# since we will trade the BLEDevice for a connectable one
# if we need to poll it
connectable=False,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
# only start after all platforms have had a chance to subscribe
coordinator.async_start()
)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
95 changes: 95 additions & 0 deletions custom_components/medisanabp_ble/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Config flow for MedisanaBP BLE integration."""

from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow
from homeassistant.data_entry_flow import FlowResult
from homeassistant.const import CONF_ADDRESS

from .medisana_bp import MedisanaBPBluetoothDeviceData
from .const import DOMAIN


class MedisanaBPConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for MedisanaBP."""

VERSION = 1

def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_device: MedisanaBPBluetoothDeviceData | None = None
self._discovered_devices: dict[str, str] = {}

async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
device = MedisanaBPBluetoothDeviceData()
if not device.supported(discovery_info):
return self.async_abort(reason="not_supported")
self._discovery_info = discovery_info
self._discovered_device = device
return await self.async_step_bluetooth_confirm()

async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
assert self._discovered_device is not None
device = self._discovered_device
assert self._discovery_info is not None
discovery_info = self._discovery_info
title = device.title or device.get_device_name() or discovery_info.name
if user_input is not None:
return self.async_create_entry(title=title, data={})

self._set_confirm_only()
placeholders = {"name": title}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="bluetooth_confirm", description_placeholders=placeholders
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user step to pick discovered device."""
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self._discovered_devices[address], data={}
)

current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
device = MedisanaBPBluetoothDeviceData()
if device.supported(discovery_info):
self._discovered_devices[address] = (
device.title or device.get_device_name() or discovery_info.name
)

if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
),
)
3 changes: 3 additions & 0 deletions custom_components/medisanabp_ble/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for MedisanaBP BLE."""

DOMAIN = "medisanabp_ble"
16 changes: 16 additions & 0 deletions custom_components/medisanabp_ble/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Constants for MedisanaBP BLE."""

from __future__ import annotations

from .medisana_bp import DeviceKey

from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothEntityKey,
)


def device_key_to_bluetooth_entity_key(
device_key: DeviceKey,
) -> PassiveBluetoothEntityKey:
"""Convert a device key to an entity key."""
return PassiveBluetoothEntityKey(device_key.key, device_key.device_id)
18 changes: 18 additions & 0 deletions custom_components/medisanabp_ble/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"domain": "medisanabp_ble",
"name": "Medisana Blood Pressure BLE",
"config_flow": true,
"documentation": "https://github.com/bkbilly/medisanabp_ble",
"issue_tracker": "https://github.com/bkbilly/medisanabp_ble/issues",
"bluetooth": [
{
"manufacturer_id": 18498,
"connectable": true
}
],
"codeowners": ["@bkbilly"],
"iot_class": "local_push",
"dependencies": ["bluetooth_adapters"],
"requirements": [],
"version": "0.1.0"
}
30 changes: 30 additions & 0 deletions custom_components/medisanabp_ble/medisana_bp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Parser for MedisanaBP BLE advertisements"""
from __future__ import annotations

from sensor_state_data import (
BinarySensorDeviceClass,
BinarySensorValue,
DeviceKey,
SensorDescription,
SensorDeviceClass,
SensorDeviceInfo,
SensorUpdate,
SensorValue,
Units,
)

from .parser import MedisanaBPBluetoothDeviceData, MedisanaBPSensor

__version__ = "0.1.0"

__all__ = [
"MedisanaBPSensor",
"MedisanaBPBluetoothDeviceData",
"BinarySensorDeviceClass",
"DeviceKey",
"SensorUpdate",
"SensorDeviceClass",
"SensorDeviceInfo",
"SensorValue",
"Units",
]
5 changes: 5 additions & 0 deletions custom_components/medisanabp_ble/medisana_bp/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for MedisanaBP BLE parser"""

CHARACTERISTIC_BLOOD_PRESSURE = "00002A35-0000-1000-8000-00805f9b34fb"
CHARACTERISTIC_BATTERY = "00002A19-0000-1000-8000-00805F9B34FB"
UPDATE_INTERVAL = 10
Loading

0 comments on commit 056ac4d

Please sign in to comment.