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