diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/LICENSE.md b/LICENSE.md index aa06fe9..8f7a935 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,7 @@ MIT License Copyright (c) 2024 Vasilis Koulis +Copyright (c) 2024 Kevin McDonald 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: diff --git a/README.md b/README.md index e191868..94ae482 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,53 @@ -[![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) +[![GitHub Release](https://img.shields.io/github/release/k3vmcd/ha-tirelinc.svg?style=flat-square)](https://github.com/k3vmcd/ha-tirelinc/releases) +[![License](https://img.shields.io/github/license/k3vmcd/ha-tirelinc.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. +# Home Assistant TireLinc Integration +Integrates TireLinc TPMS (https://www.lippert.com/rv-camping/collections/tire-linc) to Home Assistant using active connection to get information from the sensors. + +Current Limitations: + - You MUST successfully pair the tires to the central TireLinc repeater using the manufacturer device before data will be received by this integration. The tires report their sensor data on the 433MHz band and the central repeater unit translates that into the Bluetooth signal required by this integration. This integration will NOT read the 433MHz data directly. + - Scans 4 tires only. May throw errors with 2 tires and currently will not scan 6 tires. (The user may manually adjust the code to edit the number of tires scanned by editing `./sensor.py`, `./tirelinc/const.py`, and `./tirelinc/parser.py`). + - Code that could possibly expose the configured alert thresholds is currently unused and will not expose these sensors (unless user manually adds the sensors with additional edits to sensor.py). It is left in there to decode what was discovered in reverse engineering the hex data bytes. These thresholds - min/max pressure, max temperature, and max temperature change alerts - are configured in the manufactuerer device. The expectation of this integration is the user would configure native Home Assistant automations and set their own, separate thresholds within Home Assistant and therefore these manufacturer data would be irrelevant/confusing. + - There is a known issue with polling frequency. During testing, the polling interval is shown to be quite random and range from 3 minutes up to more than 1 hour in between updates. + - The TireLinc system regularly "loses" sensors and it is not possible for this integration to correct this shortcoming. Usually the TireLinc system should update every 15 minutes when stationary and every 5 minutes when moving. However, sometimes specific sensors do not report in until they move, and even then their update frequency is not always a consistent 5 minute interval. Potentially this is related to signal/noise ratio between the tire sensors and the central repeater. Yet, for the developer, this integration is shown to be more reliable than the manufacturer unit to report the most current data the central repeater has on each tire. + Exposes the following sensors: - - Battery - - Diastolic pressure - - Systolic pressure - - Pulses - - Measured date + - Tire 1 Pressure + - Tire 1 Temperature + - Tire 2 Pressure + - Tire 2 Temperature + - Tire 3 Pressure + - Tire 3 Temperature + - Tire 4 Pressure + - Tire 4 Temperature + +In a 4 tire setup, the tire locations will be: + - Tire 1: Front Left + - Tire 2: Rear Left + - Tire 3: Front Right + - Tire 4: Rear Right ## 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) +[![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=k3vmcd&repository=ha-tirelinc&category=integration) -`HACS -> Explore & Add Repositories -> Medisana Blood Pressure BLE` +`HACS -> Explore & Add Repositories -> TireLinc` The device will be autodiscovered once the data are received by any bluetooth proxy. + +If you are using an ESPHome device to connect to TireLinc, ensure you have it configured with: + +``` +bluetooth_proxy: + active: True +``` +and, as the ESPHome docs suggest to improve RAM management: +``` + framework: + type: esp-idf +``` diff --git a/custom_components/medisanabp_ble/const.py b/custom_components/medisanabp_ble/const.py deleted file mode 100644 index e8978fd..0000000 --- a/custom_components/medisanabp_ble/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for MedisanaBP BLE.""" - -DOMAIN = "medisanabp_ble" diff --git a/custom_components/medisanabp_ble/manifest.json b/custom_components/medisanabp_ble/manifest.json deleted file mode 100644 index 5bb7ff7..0000000 --- a/custom_components/medisanabp_ble/manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "domain": "medisanabp_ble", - "name": "Medisana Blood Pressure BLE", - "bluetooth": [ - { - "manufacturer_id": 18498, - "connectable": true - } - ], - "codeowners": ["@bkbilly"], - "config_flow": true, - "dependencies": ["bluetooth_adapters"], - "documentation": "https://github.com/bkbilly/medisanabp_ble", - "iot_class": "local_push", - "issue_tracker": "https://github.com/bkbilly/medisanabp_ble/issues", - "requirements": [], - "version": "0.1.0" -} diff --git a/custom_components/medisanabp_ble/medisana_bp/const.py b/custom_components/medisanabp_ble/medisana_bp/const.py deleted file mode 100644 index fbbdc0e..0000000 --- a/custom_components/medisanabp_ble/medisana_bp/const.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Constants for MedisanaBP BLE parser""" - -CHARACTERISTIC_BLOOD_PRESSURE = "00002A35-0000-1000-8000-00805f9b34fb" -CHARACTERISTIC_BATTERY = "00002A19-0000-1000-8000-00805F9B34FB" -UPDATE_INTERVAL = 10 diff --git a/custom_components/medisanabp_ble/medisana_bp/parser.py b/custom_components/medisanabp_ble/medisana_bp/parser.py deleted file mode 100644 index 25b23a1..0000000 --- a/custom_components/medisanabp_ble/medisana_bp/parser.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -import logging -import asyncio -from datetime import datetime, timezone - -from bleak import BLEDevice -from bleak_retry_connector import ( - BleakClientWithServiceCache, - establish_connection, - retry_bluetooth_connection_error, -) -from bluetooth_data_tools import short_address -from bluetooth_sensor_state_data import BluetoothData -from home_assistant_bluetooth import BluetoothServiceInfo -from sensor_state_data import SensorDeviceClass, SensorUpdate, Units -from sensor_state_data.enum import StrEnum - -from .const import ( - CHARACTERISTIC_BLOOD_PRESSURE, - CHARACTERISTIC_BATTERY, - UPDATE_INTERVAL, -) - -_LOGGER = logging.getLogger(__name__) - - -class MedisanaBPSensor(StrEnum): - - SYSTOLIC = "systolic" - DIASTOLIC = "diastolic" - PULSE = "pulse" - SIGNAL_STRENGTH = "signal_strength" - BATTERY_PERCENT = "battery_percent" - TIMESTAMP = "timestamp" - - -class MedisanaBPBluetoothDeviceData(BluetoothData): - """Data for MedisanaBP BLE sensors.""" - - def __init__(self) -> None: - super().__init__() - self._event = asyncio.Event() - - def _start_update(self, service_info: BluetoothServiceInfo) -> None: - """Update from BLE advertisement data.""" - _LOGGER.debug("Parsing MedisanaBP BLE advertisement data: %s", service_info) - self.set_device_manufacturer("Medisana") - self.set_device_type("Blood Pressure Measurement") - name = f"{service_info.name} {short_address(service_info.address)}" - self.set_device_name(name) - self.set_title(name) - - def poll_needed( - self, service_info: BluetoothServiceInfo, last_poll: float | None - ) -> bool: - """ - This is called every time we get a service_info for a device. It means the - device is working and online. - """ - return not last_poll or last_poll > UPDATE_INTERVAL - - @retry_bluetooth_connection_error() - def notification_handler(self, _, data) -> None: - """Helper for command events""" - syst = data[2] * 256 + data[1] - diast = data[4] * 256 + data[3] - arter = data[6] * 256 + data[5] - dyear = data[8] * 256 + data[7] - dmonth = data[9] - dday = data[10] - dhour = data[11] - dminu = data[12] - puls = data[15] * 256 + data[14] - user = data[16] - try: - datetime_str = f"{dyear}/{dmonth}/{dday} {dhour}:{dminu:0>2}" - date = datetime.strptime(datetime_str, '%Y/%m/%d %H:%M') - local_timezone = datetime.now(timezone.utc).astimezone().tzinfo - self.update_sensor( - key=str(MedisanaBPSensor.TIMESTAMP), - native_unit_of_measurement=None, - native_value=date.replace(tzinfo=local_timezone), - name="Measured Date", - ) - except: - _LOGGER.error("Can't add Measured Date") - - _LOGGER.info( - "Got data from BPM device (syst: %s, diast: %s, puls: %s)", - syst, diast, puls) - - self.update_sensor( - key=str(MedisanaBPSensor.SYSTOLIC), - native_unit_of_measurement=Units.PRESSURE_MMHG, - native_value=syst, - device_class=SensorDeviceClass.PRESSURE, - name="Systolic", - ) - self.update_sensor( - key=str(MedisanaBPSensor.DIASTOLIC), - native_unit_of_measurement=Units.PRESSURE_MMHG, - native_value=diast, - device_class=SensorDeviceClass.PRESSURE, - name="Diastolic", - ) - self.update_sensor( - key=str(MedisanaBPSensor.PULSE), - native_unit_of_measurement="bpm", - native_value=puls, - name="Pulse", - ) - self._event.set() - return - - async def async_poll(self, ble_device: BLEDevice) -> SensorUpdate: - """ - Poll the device to retrieve any values we can't get from passive listening. - """ - _LOGGER.debug("Connecting to BLE device: %s", ble_device.address) - client = await establish_connection( - BleakClientWithServiceCache, ble_device, ble_device.address - ) - try: - await client.start_notify( - CHARACTERISTIC_BLOOD_PRESSURE, self.notification_handler - ) - except: - _LOGGER.warn("Notify Bleak error") - - battery_char = client.services.get_characteristic(CHARACTERISTIC_BATTERY) - battery_payload = await client.read_gatt_char(battery_char) - self.update_sensor( - key=str(MedisanaBPSensor.BATTERY_PERCENT), - native_unit_of_measurement=Units.PERCENTAGE, - native_value=battery_payload[0], - device_class=SensorDeviceClass.BATTERY, - name="Battery", - ) - - # Wait to see if a callback comes in. - try: - await asyncio.wait_for(self._event.wait(), 15) - except asyncio.TimeoutError: - _LOGGER.warn("Timeout getting command data.") - except: - _LOGGER.warn("Wait For Bleak error") - finally: - await client.stop_notify(CHARACTERISTIC_BLOOD_PRESSURE) - await client.disconnect() - _LOGGER.debug("Disconnected from active bluetooth client") - return self._finish_update() diff --git a/custom_components/medisanabp_ble/__init__.py b/custom_components/tirelinc/__init__.py similarity index 90% rename from custom_components/medisanabp_ble/__init__.py rename to custom_components/tirelinc/__init__.py index 829869e..14764be 100644 --- a/custom_components/medisanabp_ble/__init__.py +++ b/custom_components/tirelinc/__init__.py @@ -1,4 +1,4 @@ -"""The MedisanaBP integration.""" +"""The TireLinc integration.""" from __future__ import annotations @@ -17,7 +17,7 @@ from homeassistant.const import Platform from homeassistant.core import CoreState, HomeAssistant -from .medisana_bp import MedisanaBPBluetoothDeviceData, SensorUpdate +from .tirelinc import TireLincBluetoothDeviceData, SensorUpdate from .const import DOMAIN PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,10 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up MedisanaBP BLE device from a config entry.""" + """Set up TireLinc BLE device from a config entry.""" address = entry.unique_id assert address is not None - data = MedisanaBPBluetoothDeviceData() + data = TireLincBluetoothDeviceData() def _needs_poll( service_info: BluetoothServiceInfoBleak, last_poll: float | None @@ -47,8 +47,6 @@ def _needs_poll( ) 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: diff --git a/custom_components/medisanabp_ble/config_flow.py b/custom_components/tirelinc/config_flow.py similarity index 89% rename from custom_components/medisanabp_ble/config_flow.py rename to custom_components/tirelinc/config_flow.py index 8268513..e2efa62 100644 --- a/custom_components/medisanabp_ble/config_flow.py +++ b/custom_components/tirelinc/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for MedisanaBP BLE integration.""" +"""Config flow for TireLinc integration.""" from __future__ import annotations @@ -14,19 +14,19 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.const import CONF_ADDRESS -from .medisana_bp import MedisanaBPBluetoothDeviceData +from .tirelinc import TireLincBluetoothDeviceData from .const import DOMAIN -class MedisanaBPConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for MedisanaBP.""" +class TireLincConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for TireLinc""" VERSION = 1 def __init__(self) -> None: """Initialize the config flow.""" self._discovery_info: BluetoothServiceInfoBleak | None = None - self._discovered_device: MedisanaBPBluetoothDeviceData | None = None + self._discovered_device: TireLincBluetoothDeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( @@ -35,7 +35,7 @@ async def async_step_bluetooth( """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() - device = MedisanaBPBluetoothDeviceData() + device = TireLincBluetoothDeviceData() if not device.supported(discovery_info): return self.async_abort(reason="not_supported") self._discovery_info = discovery_info @@ -78,7 +78,7 @@ async def async_step_user( address = discovery_info.address if address in current_addresses or address in self._discovered_devices: continue - device = MedisanaBPBluetoothDeviceData() + device = TireLincBluetoothDeviceData() if device.supported(discovery_info): self._discovered_devices[address] = ( device.title or device.get_device_name() or discovery_info.name diff --git a/custom_components/tirelinc/const.py b/custom_components/tirelinc/const.py new file mode 100644 index 0000000..e7621e2 --- /dev/null +++ b/custom_components/tirelinc/const.py @@ -0,0 +1,3 @@ +"""Constants for TireLinc""" + +DOMAIN = "tirelinc" diff --git a/custom_components/medisanabp_ble/device.py b/custom_components/tirelinc/device.py similarity index 83% rename from custom_components/medisanabp_ble/device.py rename to custom_components/tirelinc/device.py index 331a237..cb5041f 100644 --- a/custom_components/medisanabp_ble/device.py +++ b/custom_components/tirelinc/device.py @@ -1,8 +1,8 @@ -"""Constants for MedisanaBP BLE.""" +"""Constants for TireLinc""" from __future__ import annotations -from .medisana_bp import DeviceKey +from .tirelinc import DeviceKey from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothEntityKey, diff --git a/custom_components/tirelinc/manifest.json b/custom_components/tirelinc/manifest.json new file mode 100644 index 0000000..b0a15a9 --- /dev/null +++ b/custom_components/tirelinc/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "tirelinc", + "name": "TireLinc TPMS", + "bluetooth": [ + { + "local_name": "TireLinc*", + "connectable": true + } + ], + "codeowners": ["@k3vmcd"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://github.com/k3vmcd/ha-tirelinc", + "iot_class": "local_push", + "issue_tracker": "https://github.com/k3vmcd/ha-tirelinc/issues", + "requirements": [], + "version": "0.1.0" +} diff --git a/custom_components/medisanabp_ble/sensor.py b/custom_components/tirelinc/sensor.py similarity index 56% rename from custom_components/medisanabp_ble/sensor.py rename to custom_components/tirelinc/sensor.py index e9ff7ff..5db1802 100644 --- a/custom_components/medisanabp_ble/sensor.py +++ b/custom_components/tirelinc/sensor.py @@ -1,8 +1,8 @@ -"""Support for MedisanaBP sensors.""" +"""Support for TireLinc sensors.""" from __future__ import annotations -from .medisana_bp import MedisanaBPSensor, SensorUpdate +from .tirelinc import TireLincSensor, SensorUpdate from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -18,10 +18,11 @@ SensorStateClass, ) from homeassistant.const import ( - PERCENTAGE, + # PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfPressure, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,43 +34,74 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { - MedisanaBPSensor.SYSTOLIC: SensorEntityDescription( - key=MedisanaBPSensor.SYSTOLIC, - native_unit_of_measurement=UnitOfPressure.MMHG, + TireLincSensor.TIRE1_PRESSURE: SensorEntityDescription( + key=TireLincSensor.TIRE1_PRESSURE, + native_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, - icon="mdi:water-minus", + icon="mdi:car-tire-alert", ), - MedisanaBPSensor.DIASTOLIC: SensorEntityDescription( - key=MedisanaBPSensor.DIASTOLIC, - native_unit_of_measurement=UnitOfPressure.MMHG, + TireLincSensor.TIRE1_TEMPERATURE: SensorEntityDescription( + key=TireLincSensor.TIRE1_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-lines", + ), + TireLincSensor.TIRE2_PRESSURE: SensorEntityDescription( + key=TireLincSensor.TIRE2_PRESSURE, + native_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + icon="mdi:car-tire-alert", + ), + TireLincSensor.TIRE2_TEMPERATURE: SensorEntityDescription( + key=TireLincSensor.TIRE2_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-lines", + ), + TireLincSensor.TIRE3_PRESSURE: SensorEntityDescription( + key=TireLincSensor.TIRE3_PRESSURE, + native_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, - icon="mdi:water-plus", + icon="mdi:car-tire-alert", + ), + TireLincSensor.TIRE3_TEMPERATURE: SensorEntityDescription( + key=TireLincSensor.TIRE3_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-lines", ), - MedisanaBPSensor.PULSE: SensorEntityDescription( - key=MedisanaBPSensor.PULSE, - native_unit_of_measurement="bpm", - icon="mdi:heart-flash", + TireLincSensor.TIRE4_PRESSURE: SensorEntityDescription( + key=TireLincSensor.TIRE4_PRESSURE, + native_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + icon="mdi:car-tire-alert", ), - MedisanaBPSensor.SIGNAL_STRENGTH: SensorEntityDescription( - key=MedisanaBPSensor.SIGNAL_STRENGTH, + TireLincSensor.TIRE4_TEMPERATURE: SensorEntityDescription( + key=TireLincSensor.TIRE4_TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-lines", + ), + TireLincSensor.SIGNAL_STRENGTH: SensorEntityDescription( + key=TireLincSensor.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - MedisanaBPSensor.BATTERY_PERCENT: SensorEntityDescription( - key=MedisanaBPSensor.BATTERY_PERCENT, - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - MedisanaBPSensor.TIMESTAMP: SensorEntityDescription( - key=MedisanaBPSensor.TIMESTAMP, - device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-time-four-outline", - ), + # TireLincSensor.BATTERY_PERCENT: SensorEntityDescription( + # key=TireLincSensor.BATTERY_PERCENT, + # device_class=SensorDeviceClass.BATTERY, + # native_unit_of_measurement=PERCENTAGE, + # state_class=SensorStateClass.MEASUREMENT, + # entity_category=EntityCategory.DIAGNOSTIC, + # ), + # TireLincSensor.TIMESTAMP: SensorEntityDescription( + # key=TireLincSensor.TIMESTAMP, + # device_class=SensorDeviceClass.TIMESTAMP, + # icon="mdi:clock-time-four-outline", + # ), } @@ -105,14 +137,14 @@ async def async_setup_entry( entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the MedisanaBP BLE sensors.""" + """Set up the TireLinc BLE sensors.""" coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( - MedisanaBPBluetoothSensorEntity, async_add_entities + TireLincBluetoothSensorEntity, async_add_entities ) ) entry.async_on_unload( @@ -120,11 +152,11 @@ async def async_setup_entry( ) -class MedisanaBPBluetoothSensorEntity( +class TireLincBluetoothSensorEntity( PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[str | int | None]], SensorEntity, ): - """Representation of a MedisanaBP sensor.""" + """Representation of a TireLinc sensor.""" @property def native_value(self) -> str | int | None: diff --git a/custom_components/medisanabp_ble/strings.json b/custom_components/tirelinc/strings.json similarity index 100% rename from custom_components/medisanabp_ble/strings.json rename to custom_components/tirelinc/strings.json diff --git a/custom_components/medisanabp_ble/medisana_bp/__init__.py b/custom_components/tirelinc/tirelinc/__init__.py similarity index 71% rename from custom_components/medisanabp_ble/medisana_bp/__init__.py rename to custom_components/tirelinc/tirelinc/__init__.py index b8e6d73..83a20b4 100644 --- a/custom_components/medisanabp_ble/medisana_bp/__init__.py +++ b/custom_components/tirelinc/tirelinc/__init__.py @@ -1,4 +1,4 @@ -"""Parser for MedisanaBP BLE advertisements""" +"""Parser for TireLinc advertisements""" from __future__ import annotations from sensor_state_data import ( @@ -13,13 +13,13 @@ Units, ) -from .parser import MedisanaBPBluetoothDeviceData, MedisanaBPSensor +from .parser import TireLincBluetoothDeviceData, TireLincSensor __version__ = "0.1.0" __all__ = [ - "MedisanaBPSensor", - "MedisanaBPBluetoothDeviceData", + "TireLincSensor", + "TireLincBluetoothDeviceData", "BinarySensorDeviceClass", "DeviceKey", "SensorUpdate", diff --git a/custom_components/tirelinc/tirelinc/const.py b/custom_components/tirelinc/tirelinc/const.py new file mode 100644 index 0000000..a3aeb14 --- /dev/null +++ b/custom_components/tirelinc/tirelinc/const.py @@ -0,0 +1,10 @@ +"""Constants for TireLinc parser""" + +CHARACTERISTIC_TIRELINC_SENSORS = "00000002-00b7-4807-beee-e0b0879cf3dd" +# CHARACTERISTIC_BATTERY = "00002A19-0000-1000-8000-00805F9B34FB" +UPDATE_INTERVAL = 10 +TIRE1_SENSOR_ID = "0E-B3-0B-02" +TIRE2_SENSOR_ID = "0E-88-46-02" +TIRE3_SENSOR_ID = "0E-FF-47-02" +TIRE4_SENSOR_ID = "0E-61-3A-02" +EXPECTED_NOTIFICATION_COUNT = 10 \ No newline at end of file diff --git a/custom_components/tirelinc/tirelinc/parser.py b/custom_components/tirelinc/tirelinc/parser.py new file mode 100644 index 0000000..d6e2783 --- /dev/null +++ b/custom_components/tirelinc/tirelinc/parser.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import logging +import asyncio +from datetime import datetime, timezone + +from bleak import BLEDevice +from bleak_retry_connector import ( + BleakClientWithServiceCache, + establish_connection, + retry_bluetooth_connection_error, +) +from bluetooth_data_tools import short_address +from bluetooth_sensor_state_data import BluetoothData +from home_assistant_bluetooth import BluetoothServiceInfo +from sensor_state_data import SensorDeviceClass, SensorUpdate, Units +from sensor_state_data.enum import StrEnum + +from .const import ( + CHARACTERISTIC_TIRELINC_SENSORS, + # CHARACTERISTIC_BATTERY, + UPDATE_INTERVAL, + TIRE1_SENSOR_ID, + TIRE2_SENSOR_ID, + TIRE3_SENSOR_ID, + TIRE4_SENSOR_ID, + EXPECTED_NOTIFICATION_COUNT, +) + +_LOGGER = logging.getLogger(__name__) + + +class TireLincSensor(StrEnum): + + PRESSURE = "pressure" + TIRE1_PRESSURE = "tire1_pressure" + TIRE2_PRESSURE = "tire2_pressure" + TIRE3_PRESSURE = "tire3_pressure" + TIRE4_PRESSURE = "tire4_pressure" + TIRE1_TEMPERATURE = "tire1_temperature" + TIRE2_TEMPERATURE = "tire2_temperature" + TIRE3_TEMPERATURE = "tire3_temperature" + TIRE4_TEMPERATURE = "tire4_temperature" + SIGNAL_STRENGTH = "signal_strength" + # BATTERY_PERCENT = "battery_percent" + # TIMESTAMP = "timestamp" + +class TireLincBluetoothDeviceData(BluetoothData): + """Data for TireLinc sensors.""" + + def __init__(self) -> None: + super().__init__() + self._event = asyncio.Event() + self._notification_count = 0 + + def _start_update(self, service_info: BluetoothServiceInfo) -> None: + """Update from BLE advertisement data.""" + _LOGGER.debug("Parsing TireLinc BLE advertisement data: %s", service_info) + self.set_device_manufacturer("TireLinc") + self.set_device_type("TPMS") + name = f"{service_info.name} {short_address(service_info.address)}" + self.set_device_name(name) + self.set_title(name) + + def poll_needed( + self, service_info: BluetoothServiceInfo, last_poll: float | None + ) -> bool: + """ + This is called every time we get a service_info for a device. It means the + device is working and online. + """ + _LOGGER.warn("Last poll: %s", last_poll) + _LOGGER.warn("Update interval: %s", UPDATE_INTERVAL) + return not last_poll or last_poll > UPDATE_INTERVAL + + # @retry_bluetooth_connection_error() + def notification_handler(self, _, data) -> None: + """Helper for command events""" + try: + TIRE1_SENSOR_ID_bytes = bytes.fromhex(TIRE1_SENSOR_ID.replace("-", "")) + TIRE2_SENSOR_ID_bytes = bytes.fromhex(TIRE2_SENSOR_ID.replace("-", "")) + TIRE3_SENSOR_ID_bytes = bytes.fromhex(TIRE3_SENSOR_ID.replace("-", "")) + TIRE4_SENSOR_ID_bytes = bytes.fromhex(TIRE4_SENSOR_ID.replace("-", "")) + + if data[0] not in [0x04, 0x01]: + sensor_id = bytes([data[1], data[2], data[3], data[4]]) + else: + sensor_id = 0 + + if sensor_id == TIRE1_SENSOR_ID_bytes: + if data[0] == 0x02: + tire1_alert_min_pressure = data[7] + tire1_alert_max_pressure = data[9] + tire1_alert_max_temperature = data[11] + tire1_alert_max_temp_change = data[13] + elif data[0] == 0x00: + tire1_temperature = data[7] + tire1_pressure = data[9] + + self.update_sensor( + key=str(TireLincSensor.TIRE1_PRESSURE), + native_unit_of_measurement=Units.PRESSURE_PSI, + native_value=tire1_pressure, + device_class=SensorDeviceClass.PRESSURE, + name="Tire 1 Pressure", + ) + + self.update_sensor( + key=str(TireLincSensor.TIRE1_TEMPERATURE), + native_unit_of_measurement=Units.TEMP_FAHRENHEIT, + native_value=tire1_temperature, + device_class=SensorDeviceClass.TEMPERATURE, + name="Tire 1 Temperature", + ) + + elif sensor_id == TIRE2_SENSOR_ID_bytes: + if data[0] == 0x02: + tire2_alert_min_pressure = data[7] + tire2_alert_max_pressure = data[9] + tire2_alert_max_temperature = data[11] + tire2_alert_max_temp_change = data[13] + elif data[0] == 0x00: + tire2_temperature = data[7] + tire2_pressure = data[9] + + self.update_sensor( + key=str(TireLincSensor.TIRE2_PRESSURE), + native_unit_of_measurement=Units.PRESSURE_PSI, + native_value=tire2_pressure, + device_class=SensorDeviceClass.PRESSURE, + name="Tire 2 Pressure", + ) + + self.update_sensor( + key=str(TireLincSensor.TIRE2_TEMPERATURE), + native_unit_of_measurement=Units.TEMP_FAHRENHEIT, + native_value=tire2_temperature, + device_class=SensorDeviceClass.TEMPERATURE, + name="Tire 2 Temperature", + ) + + elif sensor_id == TIRE3_SENSOR_ID_bytes: + if data[0] == 0x02: + tire3_alert_min_pressure = data[7] + tire3_alert_max_pressure = data[9] + tire3_alert_max_temperature = data[11] + tire3_alert_max_temp_change = data[13] + elif data[0] == 0x00: + tire3_temperature = data[7] + tire3_pressure = data[9] + + self.update_sensor( + key=str(TireLincSensor.TIRE3_PRESSURE), + native_unit_of_measurement=Units.PRESSURE_PSI, + native_value=tire3_pressure, + device_class=SensorDeviceClass.PRESSURE, + name="Tire 3 Pressure", + ) + + self.update_sensor( + key=str(TireLincSensor.TIRE3_TEMPERATURE), + native_unit_of_measurement=Units.TEMP_FAHRENHEIT, + native_value=tire3_temperature, + device_class=SensorDeviceClass.TEMPERATURE, + name="Tire 3 Temperature", + ) + + elif sensor_id == TIRE4_SENSOR_ID_bytes: + if data[0] == 0x02: + tire4_alert_min_pressure = data[7] + tire4_alert_max_pressure = data[9] + tire4_alert_max_temperature = data[11] + tire4_alert_max_temp_change = data[13] + elif data[0] == 0x00: + tire4_temperature = data[7] + tire4_pressure = data[9] + + self.update_sensor( + key=str(TireLincSensor.TIRE4_PRESSURE), + native_unit_of_measurement=Units.PRESSURE_PSI, + native_value=tire4_pressure, + device_class=SensorDeviceClass.PRESSURE, + name="Tire 4 Pressure", + ) + + self.update_sensor( + key=str(TireLincSensor.TIRE4_TEMPERATURE), + native_unit_of_measurement=Units.TEMP_FAHRENHEIT, + native_value=tire4_temperature, + device_class=SensorDeviceClass.TEMPERATURE, + name="Tire 4 Temperature", + ) + + except NameError: + # Handle when variables are not defined + pass + + # Increment the notification count + self._notification_count += 1 + _LOGGER.warn("Notification count %s", self._notification_count) + # Check if all expected notifications are processed + if self._notification_count >= EXPECTED_NOTIFICATION_COUNT: + # Reset the counter and set the event to indicate that all notifications are processed + self._notification_count = 0 + _LOGGER.warn("Notification count %s", self._notification_count) + self._event.set() + _LOGGER.warn("Event %s", self._event.is_set()) + return + + async def async_poll(self, ble_device: BLEDevice) -> SensorUpdate: + """ + Poll the device to retrieve any values we can't get from passive listening. + """ + _LOGGER.debug("Connecting to BLE device: %s", ble_device.address) + client = await establish_connection( + BleakClientWithServiceCache, ble_device, ble_device.address + ) + try: + await client.start_notify( + CHARACTERISTIC_TIRELINC_SENSORS, self.notification_handler + ) + # Wait until all notifications are processed + try: + await asyncio.wait_for(self._event.wait(), 15) + except asyncio.TimeoutError: + _LOGGER.warn("Timeout waiting for notifications to be processed") + except: + _LOGGER.warn("Notify Bleak error") + finally: + # await client.stop_notify(CHARACTERISTIC_TIRELINC_SENSORS) + await client.disconnect() + _LOGGER.debug("Disconnected from active bluetooth client") + return self._finish_update() \ No newline at end of file diff --git a/custom_components/medisanabp_ble/medisana_bp/py.typed b/custom_components/tirelinc/tirelinc/py.typed similarity index 100% rename from custom_components/medisanabp_ble/medisana_bp/py.typed rename to custom_components/tirelinc/tirelinc/py.typed diff --git a/hacs.json b/hacs.json index afcabc8..c5e9820 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "Medisana Blood Pressure BLE", + "name": "TireLinc", "render_readme": true, "homeassistant": "2023.11.0" }