Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

Binary file added MijiaV1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added MijiaV2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 42 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
# mijia-sensor-domoticz

Adapted version of miflora (https://github.com/Tristan79/miflora) for the Xiaomi Mijia Bluetooth Temperature Humidity Sensor (MJ_HT_V1).

The Xiaomi Mijia sensor provides temperature and humidity over BLE.

## Preparing Domoticz
Create a virtual sensor (Temperature & Humidity) in Domoticz for each of your Xiaomi Mijia sensors.

Note down the IDX value for the virtual sensor.

## Finding the Bluetooth MAC Address for the sensor
Turn on the sensor (insert battery).

Run the following command to find the MAC address:

`sudo hcitool lescan`

The address will be listed with the name 'MJ_HT_V1'

Note down the MAC Address for the sensor.

## Edit the domoticz_mijia.py script
Enter your domoticz connection details in the varibles at the top of the script.

Edit the 'update' lines at the end of your script, enter the IDX and MAC address for each sensor.

e.g.
`update("4C:65:A8:D0:4C:98","752")`

## Schedule the polling
Enable this script to run at a regular interval (30 mins):

`sudo crontab -e`

And then add this line:

`*/30 * * * * /usr/bin/python3 /home/pi/mijia-sensor-domoticz/domoticz_mijia.py >/dev/null 2>&1`
# mijia-sensor-domoticz

Adapted version of mijia-sensor-domoticz (https://github.com/pFenners/mijia-sensor-domoticz) for supporting the V2 version of the Xiaomi Mijia Bluetooth Temperature Humidity Sensor.

![V1Sensor](MijiaV1.jpg) ![V2Sensor](MijiaV2.jpg)

Adapted version of miflora (https://github.com/Tristan79/miflora) for the Xiaomi Mijia Bluetooth Temperature Humidity Sensor (MJ_HT_V1).

The Xiaomi Mijia sensor provides temperature and humidity over BLE.

## Preparing Domoticz
Create a virtual sensor (Temperature & Humidity) in Domoticz for each of your Xiaomi Mijia sensors.

Note down the IDX value for the virtual sensor.

## Finding the Bluetooth MAC Address for the sensor
Turn on the sensor (insert battery).

Run the following command to find the MAC address:

`sudo hcitool lescan`

The address will be listed with the name 'MJ_HT_V1'

Note down the MAC Address for the sensor.

## Edit the domoticz_mijia.py script
Enter your domoticz connection details in the varibles at the top of the script.

Edit the 'update' lines at the end of your script, enter the IDX and MAC address for each sensor.

e.g.
`update("4C:65:A8:D0:4C:98","752")`

## Schedule the polling
Enable this script to run at a regular interval (30 mins):

`sudo crontab -e`

And then add this line:

`*/30 * * * * /usr/bin/python3 /home/pi/mijia-sensor-domoticz/domoticz_mijia.py >/dev/null 2>&1`
34 changes: 26 additions & 8 deletions domoticz_mijia.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import urllib.request
import base64
import time
import btlewrap
from btlewrap.base import BluetoothBackendException
from mijia.mitemp2_bt_poller import MiTemp2BtPoller, \
MI_HUMIDITY, MI_TEMPERATURE, MI_BATTERY
from mijia.mijia_poller import MijiaPoller, \
MI_HUMIDITY, MI_TEMPERATURE, MI_BATTERY

Expand All @@ -19,6 +23,13 @@
# Create virtual sensors in dummy hardware
# type temperature & humidity

try:
import bluepy.btle # noqa: F401 pylint: disable=unused-import

BACKEND = btlewrap.BluepyBackend
except ImportError:
BACKEND = btlewrap.GatttoolBackend


base64string = base64.encodestring(('%s:%s' % (domoticzusername, domoticzpassword)).encode()).decode().replace('\n', '')

Expand All @@ -29,10 +40,14 @@ def domoticzrequest (url):
response = urllib.request.urlopen(request)
return response.read()

def update(address,idx_temp):

poller = MijiaPoller(address)

def update(address,idx_temp, version):
if 1 == version:
poller = MijiaPoller(address)
elif 2 == version:
poller = MiTemp2BtPoller(address, BACKEND)
else:
print("Unsupported Mijia sensor version\n")
return

loop = 0
try:
Expand All @@ -43,7 +58,10 @@ def update(address,idx_temp):
while loop < 2 and temp == "Not set":
print("Error reading value retry after 5 seconds...\n")
time.sleep(5)
poller = MijiaPoller(address)
if 1 == version:
poller = MijiaPoller(address)
elif 2 == version:
poller = MiTemp2BtPoller(address, BACKEND)
loop += 1
try:
temp = poller.parameter_value(MI_TEMPERATURE)
Expand Down Expand Up @@ -89,11 +107,11 @@ def update(address,idx_temp):


print("\n1: updating")
update("4C:65:A8:D0:4C:98","752")
update("A4:C1:38:A1:D5:92","752", 2)

update("4C:65:A8:D0:26:D2","753")
update("4C:65:A8:D0:26:D2","753", 1)

update("4C:65:A8:D0:57:2A","754")
update("4C:65:A8:D0:57:2A","754", 1)



Expand Down
192 changes: 192 additions & 0 deletions mijia/mitemp2_bt_poller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
""""
Read data from Mi Temp environmental (Temp and humidity) sensor.
"""

from datetime import datetime, timedelta
import logging
from threading import Lock
from btlewrap.base import BluetoothInterface, BluetoothBackendException

_HANDLE_READ_BATTERY_LEVEL = 0x001B
_HANDLE_READ_FIRMWARE_VERSION = 0x0012
_HANDLE_READ_NAME = 0x03
_HANDLE_READ_WRITE_SENSOR_DATA = 0x001C


MI_TEMPERATURE = "temperature"
MI_HUMIDITY = "humidity"
MI_BATTERY = "battery"

_LOGGER = logging.getLogger(__name__)


class MiTemp2BtPoller:
""""
A class to read data from Mi Temp plant sensors.
"""

def __init__(self, mac, backend, cache_timeout=600, retries=3, adapter='hci0'):
"""
Initialize a Mi Temp Poller for the given MAC address.
"""

self._mac = mac
self._bt_interface = BluetoothInterface(backend, adapter=adapter)
self._cache = None
self._cache_timeout = timedelta(seconds=cache_timeout)
self._last_read = None
self._fw_last_read = None
self.retries = retries
self.ble_timeout = 10
self.lock = Lock()
self._firmware_version = None
self.battery = None

def name(self):
"""Return the name of the sensor."""
with self._bt_interface.connect(self._mac) as connection:
name = connection.read_handle(_HANDLE_READ_NAME) # pylint: disable=no-member

if not name:
raise BluetoothBackendException("Could not read NAME using handle %s"
" from Mi Temp sensor %s" % (hex(_HANDLE_READ_NAME), self._mac))
return ''.join(chr(n) for n in name)

def fill_cache(self):
"""Fill the cache with new data from the sensor."""
_LOGGER.debug('Filling cache with new sensor data.')
try:
self.firmware_version()
except BluetoothBackendException:
# If a sensor doesn't work, wait 5 minutes before retrying
self._last_read = datetime.now() - self._cache_timeout + \
timedelta(seconds=300)
raise

with self._bt_interface.connect(self._mac) as connection:
try:
connection.wait_for_notification(_HANDLE_READ_WRITE_SENSOR_DATA, self,
self.ble_timeout) # pylint: disable=no-member
# If a sensor doesn't work, wait 5 minutes before retrying
except BluetoothBackendException:
self._last_read = datetime.now() - self._cache_timeout + \
timedelta(seconds=300)
return

def battery_level(self):
"""Return the battery level.

The battery level is updated when reading the firmware version. This
is done only once every 24h
"""
self.firmware_version()
return self.battery

def firmware_version(self):
"""Return the firmware version."""
if (self._firmware_version is None) or \
(datetime.now() - timedelta(hours=24) > self._fw_last_read):
self._fw_last_read = datetime.now()
with self._bt_interface.connect(self._mac) as connection:
res_firmware = connection.read_handle(_HANDLE_READ_FIRMWARE_VERSION) # pylint: disable=no-member
_LOGGER.debug('Received result for handle %s: %s',
_HANDLE_READ_FIRMWARE_VERSION, res_firmware)
res_battery = connection.read_handle(_HANDLE_READ_BATTERY_LEVEL) # pylint: disable=no-member
_LOGGER.debug('Received result for handle %s: %d',
_HANDLE_READ_BATTERY_LEVEL, res_battery)

if res_firmware is None:
self._firmware_version = None
else:
self._firmware_version = res_firmware.decode("utf-8")

if res_battery is None:
self.battery = 0
else:
self.battery = int(ord(res_battery))
return self._firmware_version

def parameter_value(self, parameter, read_cached=True):
"""Return a value of one of the monitored paramaters.

This method will try to retrieve the data from cache and only
request it by bluetooth if no cached value is stored or the cache is
expired.
This behaviour can be overwritten by the "read_cached" parameter.
"""
# Special handling for battery attribute
if parameter == MI_BATTERY:
return self.battery_level()

# Use the lock to make sure the cache isn't updated multiple times
with self.lock:
if (read_cached is False) or \
(self._last_read is None) or \
(datetime.now() - self._cache_timeout > self._last_read):
self.fill_cache()
else:
_LOGGER.debug("Using cache (%s < %s)",
datetime.now() - self._last_read,
self._cache_timeout)

if self.cache_available():
return self._parse_data()[parameter]
raise BluetoothBackendException("Could not read data from Mi Temp sensor %s" % self._mac)

def _check_data(self):
"""Ensure that the data in the cache is valid.

If it's invalid, the cache is wiped.
"""
if not self.cache_available():
return

parsed = self._parse_data()
_LOGGER.debug('Received new data from sensor: Temp=%.1f, Humidity=%.1f',
parsed[MI_TEMPERATURE], parsed[MI_HUMIDITY])

if parsed[MI_HUMIDITY] > 100: # humidity over 100 procent
self.clear_cache()
return

def clear_cache(self):
"""Manually force the cache to be cleared."""
self._cache = None
self._last_read = None

def cache_available(self):
"""Check if there is data in the cache."""
return self._cache is not None

def _parse_data(self):
data = self._cache

res = dict()


res[MI_TEMPERATURE] = round(int.from_bytes([data[0], data[1]], "little", signed=True)/100.0, 1)
res[MI_HUMIDITY] = int.from_bytes([data[2]], "little")

return res

@staticmethod
def _format_bytes(raw_data):
"""Prettyprint a byte array."""
if raw_data is None:
return 'None'
return ' '.join([format(c, "02x") for c in raw_data]).upper()

def handleNotification(self, handle, raw_data): # pylint: disable=unused-argument,invalid-name
""" gets called by the bluepy backend when using wait_for_notification
"""
if raw_data is None:
return

self._cache = raw_data
self._check_data()
if self.cache_available():
self._last_read = datetime.now()
else:
# If a sensor doesn't work, wait 5 minutes before retrying
self._last_read = datetime.now() - self._cache_timeout + \
timedelta(seconds=300)