From 97a8d4df960c8a0c3dc9728c632e2c9921b0035b Mon Sep 17 00:00:00 2001 From: Duncan Barclay Date: Wed, 30 Dec 2020 22:22:58 +0000 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ LICENSE | 21 ++++++++ Pipfile | 13 +++++ README.md | 102 +++++++++++++++++++++++++++++++++++ lywsd03mmc/__init__.py | 5 ++ lywsd03mmc/lywsd03mmc.py | 112 +++++++++++++++++++++++++++++++++++++++ scripts/lywsd03mmc | 21 ++++++++ scripts/lywsd03mmc2csv | 34 ++++++++++++ setup.py | 22 ++++++++ 9 files changed, 334 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 README.md create mode 100644 lywsd03mmc/__init__.py create mode 100644 lywsd03mmc/lywsd03mmc.py create mode 100755 scripts/lywsd03mmc create mode 100755 scripts/lywsd03mmc2csv create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c17eb69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +dist/ +*.egg-info/ +*/__pycache__/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6a7445 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Open Home Automation + +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. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..84a9570 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +lywsd02 = {version = "=0.0.9", sys_platform = "== 'linux'"} + +[dev-packages] +pytest = "*" + +[requires] +python_version = "3.7" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f3ed2f --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# LYWSD03MMC + +A Python library for working with Xiaomi Mijia LYWSD03MMC bluetooth temperature and humidity sensors. + +Updating the firmware on the devices is *not* required. + +This package is built on top of the lywsd02 package, which may include additional useful information. + +## Installation + +This relies on bluepy installed via python pip, which itself needs libglib2 to install: +``` +sudo apt-get install python3-pip libglib2.0-dev +``` + +The LYWSD03MMC package can then be installed from [PyPi](https://pypi.org/project/lywsd03mmc/), using the following command: + +``` +pip3 install lywsd03mmc +``` + +## Finding the MAC address of the devices +From the Xiaomi Home app: +1. Go into the details of the device +2. Click on the three dots to get into the settings +3. Click "About" (near the top of the list) +4. And make a note of the MAC address shown. + +It is also possible to find the addresses of all devices by running `sudo hcitool lescan` and looking for devices labelled "LYWSD03MMC" + +## Tools + +Two helper commands are distributed here: + +### `lywsd03mmc` - Current data + +This shows the current temperature, humidity and battery level of the device. + +Example usage: +`lywsd03mmc A4:C1:38:12:34:56` + +### `lywsd03mmc2csv` - Export history + +This exports the history to a CSV file, containing the maximum and minimum temperature and humidity for each hour there is data for. + +This can be very slow, and may take up to about 10 minutes to download all the data from the device. + +Example usage: +`lywsd03mmc2csv A4:C1:38:12:34:56 --output data.csv` + +## Library Usage + +The library interface closely matches the [LYWSD02](https://github.com/h4/lywsd02) package, with the following changes: + +* Setting the time has been removed +* Battery data is available from the main data export +* An extra option has been included to estimate the time the device was started +* History data times are calculated based on the start time of the device + +### Connecting and retrieving information + +Here's an example of getting the basic information out of the device: + +``` +from lywsd03mmc import Lywsd03mmcClient +client = Lywsd03mmcClient("A4:C1:38:12:34:56") + +data = client.data +print('Temperature: ' + str(data.temperature)) +print('Humidity: ' + str(data.humidity)) +print('Battery: ' + str(data.battery)) +print('Display units: ' + client.units) +``` + +### History + +Times given in the history output are for the end of the hour in which data was recorded. + +Downloading the history data can take a significant amount of time (up to about 10 minutes). + +A property is available on the client to output data from each record retrieved, to allow you to see the progress: + +``` +# Create the client +from lywsd03mmc import Lywsd03mmcClient +client = Lywsd03mmcClient("A4:C1:38:12:34:56") + +# Enable history output +client.enable_history_progress = True + +# Retrieve the history data +history = client.history_data +``` + + +## Troubleshooting + +### Failed to connect to peripheral + +Check you are connecting to the correct MAC address, and are in range of the device. + +If those are correct, this can normally be fixed by retrying the connection. \ No newline at end of file diff --git a/lywsd03mmc/__init__.py b/lywsd03mmc/__init__.py new file mode 100644 index 0000000..a425409 --- /dev/null +++ b/lywsd03mmc/__init__.py @@ -0,0 +1,5 @@ +from .lywsd03mmc import Lywsd03mmcClient + +__all__ = ( + 'Lywsd03mmcClient' +) diff --git a/lywsd03mmc/lywsd03mmc.py b/lywsd03mmc/lywsd03mmc.py new file mode 100644 index 0000000..ddb7421 --- /dev/null +++ b/lywsd03mmc/lywsd03mmc.py @@ -0,0 +1,112 @@ +from lywsd02 import Lywsd02Client +import struct +import collections +from datetime import datetime, timedelta + +UUID_HISTORY = 'EBE0CCBC-7A0A-4B0C-8A1A-6FF2997DA3A6' # Last idx 152 READ NOTIFY + +# Create a structure to store the data in, which includes battery data +class SensorDataBattery(collections.namedtuple('SensorDataBase', ['temperature', 'humidity', 'battery'])): + __slots__ = () + +class Lywsd03mmcClient(Lywsd02Client): + + # Temperature units specific to LYWSD03MMC devices + UNITS = { + b'\x01': 'F', + b'\x00': 'C' + } + UNITS_CODES = { + 'F': b'\x01', + 'C': b'\x00' + } + + # Call the parent init with a bigger notification timeout + def __init__(self, mac, notification_timeout=15.0): + super().__init__(mac, notification_timeout) + + def _process_sensor_data(self, data): + temperature, humidity, voltage = struct.unpack_from(' 100% 2.1 --> 0 % + self._data = SensorDataBattery(temperature=temperature, humidity=humidity, battery=battery) + + # Battery data comes along with the temperature and humidity data, so just get it from there + @property + def battery(self): + return self.data.battery + + def _get_history_data(self): + # Get the time the device was first run + self.start_time + + # Work out the expected last record we'll be sent from the device. + # The current hour doesn't appear until the end of the hour, and the time is recorded as + # the end of hour time + expected_end = datetime.now() - timedelta(hours=1) + + self._latest_record = False + with self.connect(): + self._subscribe(UUID_HISTORY, self._process_history_data) + + while True: + if not self._peripheral.waitForNotifications( + self._notification_timeout): + break + + # Find the last date we have data for, and check if it's for the current hour + if self._latest_record and self._latest_record >= expected_end: + break + + def _process_history_data(self, data): + (idx, ts, max_temp, max_hum, min_temp, min_hum) = struct.unpack_from('