Initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
*/__pycache__/*
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
||||||
13
Pipfile
Normal file
13
Pipfile
Normal file
@ -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"
|
||||||
102
README.md
Normal file
102
README.md
Normal file
@ -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.
|
||||||
5
lywsd03mmc/__init__.py
Normal file
5
lywsd03mmc/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .lywsd03mmc import Lywsd03mmcClient
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Lywsd03mmcClient'
|
||||||
|
)
|
||||||
112
lywsd03mmc/lywsd03mmc.py
Normal file
112
lywsd03mmc/lywsd03mmc.py
Normal file
@ -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('<hBh', data)
|
||||||
|
temperature /= 100
|
||||||
|
voltage /= 1000
|
||||||
|
|
||||||
|
# Estimate the battery percentage remaining
|
||||||
|
battery = min(int(round((voltage - 2.1),2) * 100), 100) # 3.1 or above --> 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('<IIhBhB', data)
|
||||||
|
|
||||||
|
# Work out the time of this record by adding the record time to time the device was started
|
||||||
|
ts = self.start_time + timedelta(seconds=ts)
|
||||||
|
min_temp /= 10
|
||||||
|
max_temp /= 10
|
||||||
|
|
||||||
|
self._latest_record = ts
|
||||||
|
self._history_data[idx] = [ts, min_temp, min_hum, max_temp, max_hum]
|
||||||
|
self.output_history_progress(ts, min_temp, max_temp)
|
||||||
|
|
||||||
|
# Getting history data is very slow, so output progress updates
|
||||||
|
enable_history_progress = False
|
||||||
|
def output_history_progress(self, ts, min_temp, max_temp):
|
||||||
|
if not self.enable_history_progress:
|
||||||
|
return
|
||||||
|
print("{}: {} to {}".format(ts, min_temp, max_temp))
|
||||||
|
|
||||||
|
|
||||||
|
# Locally cache the start time of the device.
|
||||||
|
# This value won't change, and caching improves the performance getting the history data
|
||||||
|
_start_time = False
|
||||||
|
|
||||||
|
# Work out the start time of the device by taking the current time, subtracting the time
|
||||||
|
# taken from the device (the run time), and adding the timezone offset.
|
||||||
|
@property
|
||||||
|
def start_time(self):
|
||||||
|
if not self._start_time:
|
||||||
|
start_time_delta = self.time[0] - datetime(1970,1,1) - timedelta(hours=self.tz_offset)
|
||||||
|
self._start_time = datetime.now() - start_time_delta
|
||||||
|
return self._start_time
|
||||||
|
|
||||||
|
|
||||||
|
# Disable setting the time and timezone.
|
||||||
|
# LYWSD03MMCs don't have visible clocks
|
||||||
|
@property
|
||||||
|
def time(self):
|
||||||
|
return super().time
|
||||||
|
@time.setter
|
||||||
|
def time(self, dt: datetime):
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tz_offset(self):
|
||||||
|
return super().tz_offset
|
||||||
|
@tz_offset.setter
|
||||||
|
def tz_offset(self, tz_offset: int):
|
||||||
|
return
|
||||||
21
scripts/lywsd03mmc
Executable file
21
scripts/lywsd03mmc
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
import lywsd03mmc
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('mac', help='MAC address of LYWSD02 device', nargs='+')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
for mac in args.mac:
|
||||||
|
try:
|
||||||
|
client = lywsd03mmc.Lywsd03mmcClient(mac)
|
||||||
|
print('Fetching data from {}'.format(mac))
|
||||||
|
data = client.data
|
||||||
|
print('Temperature: {}°C'.format(data.temperature))
|
||||||
|
print('Humidity: {}%'.format(data.humidity))
|
||||||
|
print('Battery: {}%'.format(client.battery))
|
||||||
|
print()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
34
scripts/lywsd03mmc2csv
Executable file
34
scripts/lywsd03mmc2csv
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
import lywsd03mmc
|
||||||
|
import csv
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('mac', help='MAC address of LYWSD03MMC device')
|
||||||
|
parser.add_argument('--output', help='File to output', default='output.csv')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.output, 'w') as csvfile:
|
||||||
|
c = csv.writer(csvfile)
|
||||||
|
c.writerow(["Time", "Min temperature", "Min humidity", "Max temperature", "Max humidity"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = lywsd03mmc.Lywsd03mmcClient(args.mac)
|
||||||
|
print('Fetching data from {}'.format(args.mac))
|
||||||
|
data = client.data
|
||||||
|
print('Temperature: {}'.format(data.temperature))
|
||||||
|
print('Humidity: {}%'.format(data.humidity))
|
||||||
|
print('Battery: {}%'.format(data.battery))
|
||||||
|
print('Device start time: {}'.format(client.start_time))
|
||||||
|
print()
|
||||||
|
print('Fetching history from {}'.format(args.mac))
|
||||||
|
client.enable_history_progress = True
|
||||||
|
history = client.history_data
|
||||||
|
for i in history:
|
||||||
|
c.writerow(history[i])
|
||||||
|
print('Done')
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
22
setup.py
Normal file
22
setup.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import setuptools
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name="lywsd03mmc",
|
||||||
|
version="0.1.0",
|
||||||
|
author="Duncan Barclay",
|
||||||
|
author_email="duncan@duncanbarclay.uk",
|
||||||
|
description="Xiaomi Mijia LYWSD03MMC sensor library",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/uduncanu/lywsd03mmc",
|
||||||
|
packages=setuptools.find_packages(),
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3"
|
||||||
|
],
|
||||||
|
python_requires='>=3.6',
|
||||||
|
install_requires=['lywsd02==0.0.9'],
|
||||||
|
scripts=['scripts/lywsd03mmc', 'scripts/lywsd03mmc2csv'],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user