-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Mopeka integration Mopeka makes BLE propane tank monitors * cover * wip * wip * bump lib * strip binary sensor * all sensors * all sensors * update tests * change quality * change quality * adjust * integration_type, strict-typing
- Loading branch information
Showing
19 changed files
with
712 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"""The Mopeka integration.""" | ||
from __future__ import annotations | ||
|
||
import logging | ||
|
||
from mopeka_iot_ble import MopekaIOTBluetoothDeviceData | ||
|
||
from homeassistant.components.bluetooth import BluetoothScanningMode | ||
from homeassistant.components.bluetooth.passive_update_processor import ( | ||
PassiveBluetoothProcessorCoordinator, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant | ||
|
||
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 Mopeka BLE device from a config entry.""" | ||
address = entry.unique_id | ||
assert address is not None | ||
data = MopekaIOTBluetoothDeviceData() | ||
coordinator = hass.data.setdefault(DOMAIN, {})[ | ||
entry.entry_id | ||
] = PassiveBluetoothProcessorCoordinator( | ||
hass, | ||
_LOGGER, | ||
address=address, | ||
mode=BluetoothScanningMode.PASSIVE, | ||
update_method=data.update, | ||
) | ||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
entry.async_on_unload( | ||
coordinator.async_start() | ||
) # only start after all platforms have had a chance to subscribe | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
"""Config flow for mopeka integration.""" | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from mopeka_iot_ble import MopekaIOTBluetoothDeviceData as DeviceData | ||
import voluptuous as vol | ||
|
||
from homeassistant.components.bluetooth import ( | ||
BluetoothServiceInfoBleak, | ||
async_discovered_service_info, | ||
) | ||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import CONF_ADDRESS | ||
from homeassistant.data_entry_flow import FlowResult | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for mopeka.""" | ||
|
||
VERSION = 1 | ||
|
||
def __init__(self) -> None: | ||
"""Initialize the config flow.""" | ||
self._discovery_info: BluetoothServiceInfoBleak | None = None | ||
self._discovered_device: DeviceData | 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 = DeviceData() | ||
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 = DeviceData() | ||
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)} | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for the Mopeka integration.""" | ||
|
||
DOMAIN = "mopeka" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
"""Support for Mopeka devices.""" | ||
from __future__ import annotations | ||
|
||
from mopeka_iot_ble 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"domain": "mopeka", | ||
"name": "Mopeka", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/mopeka", | ||
"bluetooth": [ | ||
{ | ||
"service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", | ||
"manufacturer_id": 89, | ||
"manufacturer_data_start": [3], | ||
"connectable": false | ||
}, | ||
{ | ||
"service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", | ||
"manufacturer_id": 89, | ||
"manufacturer_data_start": [8], | ||
"connectable": false | ||
} | ||
], | ||
"requirements": ["mopeka_iot_ble==0.4.0"], | ||
"dependencies": ["bluetooth_adapters"], | ||
"codeowners": ["@bdraco"], | ||
"iot_class": "local_push", | ||
"integration_type": "device" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
"""Support for Mopeka sensors.""" | ||
from __future__ import annotations | ||
|
||
from mopeka_iot_ble import SensorUpdate | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.bluetooth.passive_update_processor import ( | ||
PassiveBluetoothDataProcessor, | ||
PassiveBluetoothDataUpdate, | ||
PassiveBluetoothProcessorCoordinator, | ||
PassiveBluetoothProcessorEntity, | ||
) | ||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
SensorEntity, | ||
SensorEntityDescription, | ||
SensorStateClass, | ||
) | ||
from homeassistant.const import ( | ||
PERCENTAGE, | ||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, | ||
UnitOfLength, | ||
UnitOfTemperature, | ||
) | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity import EntityCategory | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info | ||
|
||
from .const import DOMAIN | ||
from .device import device_key_to_bluetooth_entity_key | ||
|
||
SENSOR_DESCRIPTIONS = { | ||
"battery": SensorEntityDescription( | ||
key="battery", | ||
device_class=SensorDeviceClass.BATTERY, | ||
native_unit_of_measurement=PERCENTAGE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
), | ||
"battery_voltage": SensorEntityDescription( | ||
key="battery_voltage", | ||
device_class=SensorDeviceClass.VOLTAGE, | ||
native_unit_of_measurement=PERCENTAGE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
entity_registry_enabled_default=False, | ||
), | ||
"tank_level": SensorEntityDescription( | ||
key="tank_level", | ||
device_class=SensorDeviceClass.DISTANCE, | ||
native_unit_of_measurement=UnitOfLength.MILLIMETERS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
"signal_strength": SensorEntityDescription( | ||
key="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, | ||
), | ||
"reading_quality": SensorEntityDescription( | ||
key="reading_quality", | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
native_unit_of_measurement=PERCENTAGE, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
"temperature": SensorEntityDescription( | ||
key="temperature", | ||
device_class=SensorDeviceClass.TEMPERATURE, | ||
native_unit_of_measurement=UnitOfTemperature.CELSIUS, | ||
state_class=SensorStateClass.MEASUREMENT, | ||
), | ||
"accelerometer_x": SensorEntityDescription( | ||
key="accelerometer_x", | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
), | ||
"accelerometer_y": SensorEntityDescription( | ||
key="accelerometer_y", | ||
entity_category=EntityCategory.DIAGNOSTIC, | ||
), | ||
} | ||
|
||
|
||
def sensor_update_to_bluetooth_data_update( | ||
sensor_update: SensorUpdate, | ||
) -> PassiveBluetoothDataUpdate: | ||
"""Convert a sensor update to a bluetooth data update.""" | ||
return PassiveBluetoothDataUpdate( | ||
devices={ | ||
device_id: sensor_device_info_to_hass_device_info(device_info) | ||
for device_id, device_info in sensor_update.devices.items() | ||
}, | ||
entity_descriptions={ | ||
device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ | ||
device_key.key | ||
] | ||
for device_key in sensor_update.entity_descriptions | ||
if device_key.key in SENSOR_DESCRIPTIONS | ||
}, | ||
entity_data={ | ||
device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value | ||
for device_key, sensor_values in sensor_update.entity_values.items() | ||
}, | ||
entity_names={ | ||
device_key_to_bluetooth_entity_key(device_key): sensor_values.name | ||
for device_key, sensor_values in sensor_update.entity_values.items() | ||
}, | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
entry: config_entries.ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Set up the Mopeka 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( | ||
MopekaBluetoothSensorEntity, async_add_entities | ||
) | ||
) | ||
entry.async_on_unload(coordinator.async_register_processor(processor)) | ||
|
||
|
||
class MopekaBluetoothSensorEntity( | ||
PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], | ||
SensorEntity, | ||
): | ||
"""Representation of a Mopeka sensor.""" | ||
|
||
@property | ||
def native_value(self) -> int | float | None: | ||
"""Return the native value.""" | ||
return self.processor.entity_data.get(self.entity_key) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"config": { | ||
"flow_title": "[%key:component::bluetooth::config::flow_title%]", | ||
"step": { | ||
"user": { | ||
"description": "[%key:component::bluetooth::config::step::user::description%]", | ||
"data": { | ||
"address": "[%key:component::bluetooth::config::step::user::data::address%]" | ||
} | ||
}, | ||
"bluetooth_confirm": { | ||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" | ||
} | ||
}, | ||
"abort": { | ||
"not_supported": "Device not supported", | ||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", | ||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.