├── .github └── FUNDING.yml ├── LICENSE ├── README.md └── custom_components └── xiaomi_hygrothermo ├── manifest.json └── sensor.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: dolezsa # Replace with a single Ko-fi username 4 | custom: ['https://paypal.me/dolezsa'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dolezsa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xiaomi_Hygrothermo 2 | 3 | Home-assistant sensor platform for Xiaomi Mijia BT Hygrothermo temperature and humidity sensor. 4 | 5 | ## Usage 6 | 7 | To use, add the following to your `configuration.yaml` file: 8 | 9 | ``` 10 | sensor: 11 | - platform: xiaomi_hygrothermo 12 | name: mijia ht #1 13 | mac: 'xx:xx:xx:xx:xx:xx' 14 | scan_interval: 60 15 | ``` 16 | 17 | - mac is required. 18 | - default "scan" interval is every 30 seconds. 19 | 20 | ## Determine the MAC address 21 | 22 | using hcitool: 23 | ``` 24 | $ sudo hcitool lescan 25 | LE Scan ... 26 | LE Scan ... 27 | 4C:65:A8:xx:xx:xx (unknown) 28 | 4C:65:A8:xx:xx:xx MJ_HT_V1 29 | [...] 30 | ``` 31 | 32 | using bluetoothctl: 33 | ``` 34 | $ sudo bluetoothctl 35 | [NEW] Controller xx:xx:xx:xx:xx:xx homeqube [default] 36 | [bluetooth]# scan on 37 | Discovery started 38 | [CHG] Controller xx:xx:xx:xx:xx:xx Discovering: yes 39 | [NEW] Device 4C:65:A8:xx:xx:xx MJ_HT_V1 40 | [bluetooth]# 41 | [bluetooth]# quit 42 | ``` 43 | 44 | look for MJ_HT_V1 devices... 45 | -------------------------------------------------------------------------------- /custom_components/xiaomi_hygrothermo/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "xiaomi_hygrothermo", 3 | "name": "Xioami Hygrothermo", 4 | "documentation": "https://github.com/dolezsa/Xiaomi_Hygrothermo/blob/master/README.md", 5 | "dependencies": [], 6 | "codeowners": ["@dolezsa"], 7 | "requirements": ["bluepy==1.1.4"] 8 | } 9 | -------------------------------------------------------------------------------- /custom_components/xiaomi_hygrothermo/sensor.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | import voluptuous as vol 4 | import homeassistant.helpers.config_validation as cv 5 | from datetime import timedelta 6 | from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_NAME, CONF_MAC, CONF_SCAN_INTERVAL, TEMP_CELSIUS) 7 | from homeassistant.helpers.entity import Entity 8 | from homeassistant.components.sensor import PLATFORM_SCHEMA 9 | from homeassistant.helpers.event import track_time_interval 10 | from homeassistant.util.dt import utcnow 11 | 12 | REQUIREMENTS = ['bluepy==1.1.4'] 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | BT_MAC = vol.All( 17 | cv.string, 18 | vol.Length(min=17, max=17) 19 | ) 20 | 21 | SCAN_INTERVAL = timedelta(seconds=30) 22 | NAME = "Mijia BT Hygrothermograph" 23 | 24 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 25 | vol.Required(CONF_MAC, default=None): vol.Any(BT_MAC, None), 26 | vol.Optional(CONF_NAME, default=NAME): cv.string, 27 | vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, 28 | }) 29 | 30 | SENSOR_TYPES = { 31 | 'Temperature': [TEMP_CELSIUS, 'mdi:thermometer'], 32 | 'Humidity': ['%', 'mdi:water-percent'] 33 | } 34 | 35 | def setup_platform(hass, config, add_devices, discovery_info=None): 36 | """Setup the sensor platform.""" 37 | device = XiomiHygroThermo(hass, config.get(CONF_NAME), config.get(CONF_MAC)) 38 | add_devices(device.entities) 39 | 40 | track_time_interval(hass, device.get_data, config.get(CONF_SCAN_INTERVAL)) 41 | 42 | class XiomiHygroThermoDelegate(object): 43 | def __init__(self): 44 | self.temperature = None 45 | self.humidity = None 46 | self.received = False 47 | 48 | def handleNotification(self, cHandle, data): 49 | if cHandle == 14: 50 | m = re.search('T=([\d\.]*)\s+?H=([\d\.]*)', ''.join(map(chr, data))) 51 | self.temperature = m.group(1) 52 | self.humidity = m.group(2) 53 | self.received = True 54 | 55 | class XiomiHygroThermo(object): 56 | def __init__(self, hass, name, address): 57 | self.address = address 58 | self.battery = None 59 | self.temperature = None 60 | self.humidity = None 61 | self.last_battery = None 62 | 63 | self.entities = [ 64 | XiomiHygroThermoEntity(hass, name, 'Temperature'), 65 | XiomiHygroThermoEntity(hass, name, 'Humidity') 66 | ] 67 | 68 | self.get_data() 69 | 70 | def get_data(self, now = None): 71 | try: 72 | from bluepy import btle 73 | 74 | p = btle.Peripheral(self.address) 75 | 76 | #self.name = ''.join(map(chr, p.readCharacteristic(0x3))) 77 | #self.firmware = ''.join(map(chr, p.readCharacteristic(0x24))) 78 | if self.last_battery is None or (utcnow() - self.last_battery).seconds >= 3600: 79 | self.battery = p.readCharacteristic(0x18)[0] 80 | self.last_battery = utcnow() 81 | 82 | delegate = XiomiHygroThermoDelegate() 83 | p.withDelegate( delegate ) 84 | p.writeCharacteristic(0x10, bytearray([1, 0]), True) 85 | while not delegate.received: 86 | p.waitForNotifications(30.0) 87 | 88 | self.temperature = delegate.temperature 89 | self.humidity = delegate.humidity 90 | 91 | ok = True 92 | except Exception as ex: 93 | if isinstance(ex, btle.BTLEException): 94 | _LOGGER.warning("BT connection error: {}".format(ex)) 95 | else: 96 | _LOGGER.error("Unexpected error: {}".format(ex)) 97 | ok = False 98 | 99 | for i in [0, 1]: 100 | changed = self.entities[i].set_state(ok, self.battery, self.temperature if i == 0 else self.humidity) 101 | if (not now is None) and changed: 102 | self.entities[i].async_schedule_update_ha_state() 103 | 104 | class XiomiHygroThermoEntity(Entity): 105 | def __init__(self, hass, name, device_type): 106 | self.hass = hass 107 | self._name = '{} {}'.format(name, device_type) 108 | self._state = None 109 | self._is_available = True 110 | self._type = device_type 111 | self._device_state_attributes = {} 112 | self.__errcnt = 0 113 | self.__laststate = None 114 | 115 | @property 116 | def name(self): 117 | """Return the name of the device.""" 118 | return self._name 119 | 120 | @property 121 | def available(self): 122 | """Return True if entity is available.""" 123 | return self._is_available 124 | 125 | @property 126 | def should_poll(self): 127 | """Return the polling state. No polling needed.""" 128 | return False 129 | 130 | @property 131 | def device_state_attributes(self): 132 | """Return the state attributes.""" 133 | return self._device_state_attributes 134 | 135 | @property 136 | def icon(self): 137 | """Return the icon to use in the frontend.""" 138 | try: 139 | return SENSOR_TYPES.get(self._type)[1] 140 | except TypeError: 141 | return None 142 | 143 | @property 144 | def unit_of_measurement(self): 145 | """Return the unit of measurement of this entity, if any.""" 146 | try: 147 | return SENSOR_TYPES.get(self._type)[0] 148 | except TypeError: 149 | return None 150 | 151 | @property 152 | def state(self): 153 | """Return the state of the sensor.""" 154 | return self._state 155 | 156 | def set_state(self, is_available, battery, state_value): 157 | changed = False 158 | if is_available: 159 | if not battery is None: 160 | self._device_state_attributes[ATTR_BATTERY_LEVEL] = battery 161 | changed = True 162 | self._state = state_value 163 | changed = changed or self.__laststate != state_value 164 | self.__laststate = state_value 165 | self.__errcnt = 0 166 | self._is_available = True 167 | else: 168 | self.__errcnt += 1 169 | 170 | if self.__errcnt > 3: 171 | self._is_available = False 172 | changed = True 173 | 174 | return changed 175 | --------------------------------------------------------------------------------