pax_global_header00006660000000000000000000000064144400165730014516gustar00rootroot0000000000000052 comment=b9192426c0f5d56699217554ecb12d32fb1793f6 Bluetooth-Devices-ruuvitag-ble-b919242/000077500000000000000000000000001444001657300177255ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/.flake8000066400000000000000000000000551444001657300211000ustar00rootroot00000000000000[flake8] exclude = docs max-line-length = 88 Bluetooth-Devices-ruuvitag-ble-b919242/.github/000077500000000000000000000000001444001657300212655ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/.github/FUNDING.yml000066400000000000000000000000451444001657300231010ustar00rootroot00000000000000github: ["bluetooth-devices", "akx"] Bluetooth-Devices-ruuvitag-ble-b919242/.github/dependabot.yml000066400000000000000000000001511444001657300241120ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: "weekly" Bluetooth-Devices-ruuvitag-ble-b919242/.github/workflows/000077500000000000000000000000001444001657300233225ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/.github/workflows/ci.yml000066400000000000000000000041041444001657300244370ustar00rootroot00000000000000name: CI on: push: tags: - 'v*' branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: python-version: "3.9" - uses: pre-commit/action@v3.0.0 test: strategy: fail-fast: false matrix: python-version: - "3.9" - "3.10" os: - ubuntu-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - run: pip install pytest pytest-cov -e . - run: py.test --cov . --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: "3.10" - run: pip install mypy -e . - run: mypy --strict --install-types --non-interactive . build: runs-on: ubuntu-latest needs: [lint, test, mypy] steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 uses: actions/setup-python@v3 with: python-version: "3.10" - name: Install pypa/build run: python -m pip install build twine --user - name: Build a binary wheel and a source tarball run: python -m build --sdist --wheel --outdir dist/ . - name: Check the distribution run: twine check dist/* - name: Upload artifact uses: actions/upload-artifact@v2 with: name: dist path: dist - name: Publish distribution to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: print_hash: true password: ${{ secrets.PYPI_API_TOKEN }} Bluetooth-Devices-ruuvitag-ble-b919242/.gitignore000066400000000000000000000000661444001657300217170ustar00rootroot00000000000000*.log *.py[cod] *cache .coverage* /coverage.xml /dist Bluetooth-Devices-ruuvitag-ble-b919242/.pre-commit-config.yaml000066400000000000000000000013641444001657300242120ustar00rootroot00000000000000ci: autofix_prs: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: debug-statements - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-json - id: check-toml - id: check-xml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace - id: debug-statements - repo: https://github.com/asottile/pyupgrade rev: v2.37.1 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/psf/black rev: 22.6.0 hooks: - id: black Bluetooth-Devices-ruuvitag-ble-b919242/LICENSE000066400000000000000000000020561444001657300207350ustar00rootroot00000000000000MIT License Copyright (c) 2022 Aarni Koskela 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. Bluetooth-Devices-ruuvitag-ble-b919242/README.md000066400000000000000000000000571444001657300212060ustar00rootroot00000000000000# ruuvitag-ble Parser for Ruuvitag BLE devices Bluetooth-Devices-ruuvitag-ble-b919242/pyproject.toml000066400000000000000000000040301444001657300226360ustar00rootroot00000000000000[project] name = "ruuvitag-ble" version = "0.1.2" description = "Manage Ruuvitag BLE devices" authors = [ { name = "Aarni Koskela", email = "akx@iki.fi" }, ] requires-python = ">=3.9" license = "MIT" readme = "README.md" repository = "https://github.com/bluetooth-devices/ruuvitag-ble" documentation = "https://ruuvitag-ble.readthedocs.io" classifiers = [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", ] packages = [ { include = "ruuvitag_ble", from = "src" }, ] dependencies = [ "bluetooth-data-tools>=0.1", "bluetooth-sensor-state-data>=1.6", "home-assistant-bluetooth>=1.6", "sensor-state-data>=2.9", ] [project.urls] "Bug Tracker" = "https://github.com/bluetooth-devices/ruuvitag-ble/issues" "Changelog" = "https://github.com/bluetooth-devices/ruuvitag-ble/blob/main/CHANGELOG.md" [tool.semantic_release] branch = "main" version_toml = "pyproject.toml:tool.poetry.version" version_variable = "src/ruuvitag_ble/__init__.py:__version__" build_command = "pip install poetry && poetry build" [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=ruuvitag_ble --cov-report=term-missing:skip-covered" pythonpath = ["src"] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@overload", "if TYPE_CHECKING", "raise NotImplementedError", ] [tool.isort] profile = "black" known_first_party = ["ruuvitag_ble", "tests"] [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true mypy_path = "src/" no_implicit_optional = true show_error_codes = true warn_unreachable = true warn_unused_ignores = true exclude = [ 'docs/.*', 'setup.py', ] [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true [[tool.mypy.overrides]] module = "docs.*" ignore_errors = true [build-system] requires = ["hatchling"] build-backend = "hatchling.build" Bluetooth-Devices-ruuvitag-ble-b919242/setup.py000066400000000000000000000003611444001657300214370ustar00rootroot00000000000000#!/usr/bin/env python # This is a shim to allow GitHub to detect the package, build is done with hatch # Taken from https://github.com/Textualize/rich import setuptools if __name__ == "__main__": setuptools.setup(name="ruuvitag-ble") Bluetooth-Devices-ruuvitag-ble-b919242/src/000077500000000000000000000000001444001657300205145ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/src/ruuvitag_ble/000077500000000000000000000000001444001657300232045ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/src/ruuvitag_ble/__init__.py000066400000000000000000000001741444001657300253170ustar00rootroot00000000000000from .parser import RuuvitagBluetoothDeviceData __version__ = "0.1.0rc1" __all__ = [ "RuuvitagBluetoothDeviceData", ] Bluetooth-Devices-ruuvitag-ble-b919242/src/ruuvitag_ble/df5_decoder.py000066400000000000000000000042401444001657300257210ustar00rootroot00000000000000""" Decoder for RuuviTag Data Format 5 data. Based on https://github.com/ttu/ruuvitag-sensor/blob/23e6555/ruuvitag_sensor/decoder.py (MIT Licensed) """ from __future__ import annotations import math import struct class DataFormat5Decoder: def __init__(self, raw_data: bytes) -> None: if len(raw_data) < 24: raise ValueError("Data must be at least 24 bytes long for data format 5") self.data: tuple[int, ...] = struct.unpack(">BhHHhhhHBH6B", raw_data) @property def temperature_celsius(self) -> float | None: if self.data[1] == -32768: return None return round(self.data[1] / 200.0, 2) @property def humidity_percentage(self) -> float | None: if self.data[2] == 65535: return None return round(self.data[2] / 400, 2) @property def pressure_hpa(self) -> float | None: if self.data[3] == 0xFFFF: return None return round((self.data[3] + 50000) / 100, 2) @property def acceleration_vector_mg(self) -> tuple[int, int, int] | tuple[None, None, None]: ax = self.data[4] ay = self.data[5] az = self.data[6] if ax == -32768 or ay == -32768 or az == -32768: return (None, None, None) return (ax, ay, az) @property def acceleration_total_mg(self) -> float | None: ax, ay, az = self.acceleration_vector_mg if ax is None or ay is None or az is None: return None return math.sqrt(ax * ax + ay * ay + az * az) @property def battery_voltage_mv(self) -> int | None: voltage = self.data[7] >> 5 if voltage == 0b11111111111: return None return voltage + 1600 @property def tx_power_dbm(self) -> int | None: tx_power = self.data[7] & 0x001F if tx_power == 0b11111: return None return -40 + (tx_power * 2) @property def movement_counter(self) -> int: return self.data[8] @property def measurement_sequence_number(self) -> int: return self.data[9] @property def mac(self) -> str: return ":".join(f"{x:02X}" for x in self.data[10:]) Bluetooth-Devices-ruuvitag-ble-b919242/src/ruuvitag_ble/parser.py000066400000000000000000000046151444001657300250600ustar00rootroot00000000000000from __future__ import annotations import logging 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 DeviceClass, Units from ruuvitag_ble.df5_decoder import DataFormat5Decoder _LOGGER = logging.getLogger(__name__) class RuuvitagBluetoothDeviceData(BluetoothData): """Data for Ruuvitag BLE sensors.""" def _start_update(self, service_info: BluetoothServiceInfo) -> None: try: raw_data = service_info.manufacturer_data[0x0499] except (KeyError, IndexError): _LOGGER.debug("Manufacturer ID 0x0499 not found in data") return None data_format = raw_data[0] if data_format != 0x05: _LOGGER.debug("Data format not supported: %s", raw_data) return decoder = DataFormat5Decoder(raw_data) # Compute short identifier from MAC address # (preferring the MAC address the tag broadcasts). identifier = short_address(decoder.mac or service_info.address) self.set_device_type("RuuviTag") self.set_device_manufacturer("Ruuvi Innovations Ltd.") self.set_device_name(f"RuuviTag {identifier}") self.update_sensor( key=DeviceClass.TEMPERATURE, device_class=DeviceClass.TEMPERATURE, native_unit_of_measurement=Units.TEMP_CELSIUS, native_value=decoder.temperature_celsius, ) self.update_sensor( key=DeviceClass.HUMIDITY, device_class=DeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, native_value=decoder.humidity_percentage, ) self.update_sensor( key=DeviceClass.PRESSURE, device_class=DeviceClass.PRESSURE, native_unit_of_measurement=Units.PRESSURE_HPA, native_value=decoder.pressure_hpa, ) self.update_sensor( key=DeviceClass.VOLTAGE, device_class=DeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_MILLIVOLT, native_value=decoder.battery_voltage_mv, ) self.update_sensor( key="movement_counter", device_class=DeviceClass.COUNT, native_unit_of_measurement=None, native_value=decoder.movement_counter, ) Bluetooth-Devices-ruuvitag-ble-b919242/src/ruuvitag_ble/py.typed000066400000000000000000000000001444001657300246710ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/tests/000077500000000000000000000000001444001657300210675ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/tests/__init__.py000066400000000000000000000000001444001657300231660ustar00rootroot00000000000000Bluetooth-Devices-ruuvitag-ble-b919242/tests/test_parser.py000066400000000000000000000032531444001657300237770ustar00rootroot00000000000000from home_assistant_bluetooth import BluetoothServiceInfo from sensor_state_data import DeviceClass, DeviceKey from ruuvitag_ble import RuuvitagBluetoothDeviceData OUTDOOR_SENSOR_DATA = ( b"\x05\x05\xa0`\xa0\xc8\x9a\xfd4\x02\x8c\xff\x00cvriv\xde\xad{?\xef\xaf" ) INDOOR_SENSOR_DATA = ( b"\x05\x0e\xa4M~\xc8\x18\xfc\xbc\xfd\xf0\xff\xb4+\xf6\x00\x10<\xd97\x0f\xf7\xaa\x48" ) KEY_TEMPERATURE = DeviceKey(key=DeviceClass.TEMPERATURE, device_id=None) KEY_HUMIDITY = DeviceKey(key=DeviceClass.HUMIDITY, device_id=None) KEY_PRESSURE = DeviceKey(key=DeviceClass.PRESSURE, device_id=None) KEY_VOLTAGE = DeviceKey(key=DeviceClass.VOLTAGE, device_id=None) KEY_MOVEMENT = DeviceKey(key="movement_counter", device_id=None) def bytes_to_service_info(payload: bytes) -> BluetoothServiceInfo: return BluetoothServiceInfo( name="Test", address="00:00:00:00:00:00", rssi=-60, manufacturer_data={1177: payload}, service_data={}, service_uuids=[], source="", ) def test_parsing_outdoor(): device = RuuvitagBluetoothDeviceData() advertisement = bytes_to_service_info(OUTDOOR_SENSOR_DATA) assert device.supported(advertisement) up = device.update(advertisement) expected_name = "RuuviTag EFAF" assert up.devices[None].name == expected_name # Parsed from advertisement assert up.entity_values[KEY_TEMPERATURE].native_value == 7.2 # Celsius assert up.entity_values[KEY_HUMIDITY].native_value == 61.84 # % assert up.entity_values[KEY_PRESSURE].native_value == 1013.54 # hPa assert up.entity_values[KEY_VOLTAGE].native_value == 2395 # mV assert up.entity_values[KEY_MOVEMENT].native_value == 114 # count