├── README.md ├── custom_components ├── aqara.py ├── binary_sensor │ └── aqara.py ├── sensor │ └── aqara.py └── switch │ └── aqara.py └── img ├── IMG_01.png ├── IMG_02.png ├── IMG_04.png ├── IMG_05.png ├── IMG_06.png └── IMG_07.png /README.md: -------------------------------------------------------------------------------- 1 | # homeassistant-aqara 2 | Home-Assistant implementation for the Xiaomi (Aqara) gateway 3 | Supported sensors: 4 | - Temperature / Humidity - reports if temperature change reaches 0.5 ° C or the humidity change reaches 6%. 5 | - Magnet (Door / Window) 6 | - Motion 7 | - Button Switch(Each switch was shown as three individual virtual switches: ONE_CLICK, DOUBLE_CLICK and LONG_PRESS. Those virtual switches should be better used as automation triggers only) 8 | - Power Plug 9 | - Aqara Wall Switch 10 | 11 | ### INSTALLATION 12 | 1. Install Home-Assistant, 13 | 2. Enable the developer mode of the gateway. 14 | - Please follow the steps in the wiki: 15 | https://github.com/fooxy/homeassistant-aqara/wiki/Enable-dev-mode 16 | 3. Download and place aqara.py files in the home-assistant folder like this: 17 | 18 | - .homeassistant/custom_components/aqara.py 19 | - .homeassistant/custom_components/sensor/aqara.py 20 | - .homeassistant/custom_components/binary_sensor/aqara.py 21 | 22 | 4. Add the new platform in the configuration.yaml: 23 | lowercase is important, multiple gateways is not supported by now. 24 | 25 | ```yaml 26 | aqara: 27 | gateway_password: yourgatewaypassword 28 | ``` 29 | 30 | 5. restart the home assistant service, note that it may take several minutes to install the *pyCrypto* dependency during the first start. 31 | 32 | ### CUSTOMIZATION 33 | 34 | Since until now there is no way to retrieve the configured names from the 35 | gateway, Home-Assistant will display each sensor like that: 36 | - sensor.temperature_SENSORID 37 | - sensor.humidity_SENSORID 38 | - binary_sensor.magnet_SENSORID 39 | - binary_sensor.motion_SENSORID 40 | - etc. 41 | 42 | To make it readable again, create a customize.yaml file in the home-assistant folder. 43 | You can use step 7 https://goo.gl/gEVIrn to identify the sensors. 44 | 45 | - Example 46 | 47 | ```yaml 48 | sensor.temperature_158d0000fa3793: 49 | friendly_name: Living-Room T 50 | sensor.humidity_158d0000fa3793: 51 | friendly_name: Living-Room H 52 | icon: mdi:water-percent 53 | 54 | sensor.temperature_158d000108164f: 55 | friendly_name: Bedroom 1 T 56 | sensor.humidity_158d000108164f: 57 | friendly_name: Bedroom 1 H 58 | icon: mdi:water-percent 59 | 60 | ... etc. 61 | ``` 62 | 63 | 5. Add a line in the configuration.yaml: 64 | 65 | ```yaml 66 | homeassistant: 67 | # Name of the location where Home Assistant is running 68 | name: Home 69 | ... 70 | time_zone: Europe/Paris 71 | customize: !include customize.yaml 72 | ``` 73 | 74 | ### Magnet Automation example 75 | 76 | - Example configuration.yaml 77 | 78 | ```yaml 79 | binary_sensor: 80 | - platform: template 81 | sensors: 82 | door: 83 | friendly_name: Frontdoor 84 | value_template: "{{ states.binary_sensor.magnet_158d0001179ae9.state == 'open' }}" 85 | sensor_class: opening 86 | entity_id: 87 | - binary_sensor.magnet_158d0001179ae9 88 | 89 | automation: 90 | - alias: FrontDoorClosed 91 | trigger: 92 | platform: state 93 | entity_id: binary_sensor.door 94 | to: 'off' 95 | action: 96 | service: notify.TelegramNotifier 97 | data: 98 | message: Door closed 99 | - alias: FrontDoorOpened 100 | trigger: 101 | platform: state 102 | entity_id: binary_sensor.door 103 | to: 'on' 104 | action: 105 | service: notify.TelegramNotifier 106 | data: 107 | message: Door opened 108 | ``` 109 | 110 | 111 | ### TODO 112 | 113 | - multiple gateway support 114 | - include some options in the configuration file : IP, refresh frequency, etc. 115 | - generate a yaml file with discovered devices 116 | - integrate wireless switch, cube, gateway itself (turn on light / radio / etc.) 117 | -------------------------------------------------------------------------------- /custom_components/aqara.py: -------------------------------------------------------------------------------- 1 | from homeassistant.helpers.discovery import load_platform 2 | from homeassistant.const import (EVENT_HOMEASSISTANT_START, 3 | EVENT_HOMEASSISTANT_STOP) 4 | 5 | from homeassistant.helpers import config_validation as cv 6 | from homeassistant.helpers.entity import Entity 7 | 8 | import voluptuous as vol 9 | 10 | import logging 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | DOMAIN = 'aqara' 15 | 16 | REQUIREMENTS = ['https://github.com/fooxy/homeassisitant-pyAqara/archive/v0.45-alpha.zip#pyAqara==0.45'] 17 | 18 | AQARA_COMPONENTS = [ 19 | 'sensor','binary_sensor','switch', 20 | ] 21 | 22 | CONF_GATEWAY_PASSWORD = 'gateway_password' 23 | 24 | CONFIG_SCHEMA = vol.Schema({ 25 | DOMAIN: vol.Schema({ 26 | vol.Optional(CONF_GATEWAY_PASSWORD, default=''): cv.string 27 | }) 28 | }, extra=vol.ALLOW_EXTRA) 29 | 30 | 31 | def setup(hass, config): 32 | """Your controller/hub specific code.""" 33 | 34 | from pyAqara.gateway import AqaraGateway 35 | gateway = AqaraGateway() 36 | gateway.initGateway() 37 | gateway.listen(timeout=5) 38 | 39 | gateway.password = config.get(DOMAIN,{}).get(CONF_GATEWAY_PASSWORD,'') 40 | 41 | hass.data['AqaraGateway']= gateway 42 | 43 | def _stop(event): 44 | """Stop the listener queue and clean up.""" 45 | print('stop event') 46 | nonlocal gateway 47 | gateway.stop() 48 | gateway = None 49 | _LOGGER.info("Waiting for long poll to Aqara Gateway to time out") 50 | 51 | hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop) 52 | 53 | for component in AQARA_COMPONENTS: 54 | load_platform(hass, component, DOMAIN, {}, config) 55 | 56 | return True 57 | -------------------------------------------------------------------------------- /custom_components/binary_sensor/aqara.py: -------------------------------------------------------------------------------- 1 | from homeassistant.helpers.entity import Entity 2 | from homeassistant.components.binary_sensor import BinarySensorDevice 3 | import logging 4 | import json 5 | 6 | # DOMAIN = 'aqara' 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | def setup_platform(hass, config, add_devices, discovery_info=None): 12 | """Setup the sensor platform.""" 13 | 14 | # # get the gateway object from the hub component 15 | gateway = hass.data['AqaraGateway'] 16 | devices = gateway.sidsData 17 | 18 | sensorItems = [] 19 | for device in devices: 20 | if device['model'] in ['motion', 'magnet']: 21 | sensorItems.append(AqaraBinarySensor(gateway, device['sid'], device['sid'], device['model'],device['data'])) 22 | 23 | if len(sensorItems)> 0: 24 | add_devices(sensorItems) 25 | return True 26 | else: 27 | return False 28 | 29 | class AqaraBinarySensor(BinarySensorDevice,Entity): 30 | """Representation of a Binary Sensor.""" 31 | 32 | def __init__(self, aqaraGateway,deviceName,deviceSID,deviceModel,deviceData): 33 | self.gateway = aqaraGateway 34 | self.deviceName = deviceName 35 | self.deviceSID = deviceSID 36 | self.deviceModel = deviceModel 37 | self.deviceData = deviceData 38 | self.uniqueID = '{} {}'.format(deviceModel,deviceSID) 39 | 40 | self.gateway.register(self.deviceSID, self._update_callback) 41 | 42 | status = deviceData['status'] 43 | if deviceModel == 'magnet': 44 | if status =='open': 45 | self._state = True 46 | else: 47 | self._state = False 48 | elif deviceModel == 'motion': 49 | if status =='motion': 50 | self._state = True 51 | else: 52 | self._state = False 53 | else: 54 | self._state = False 55 | 56 | def _update_callback(self, model,sid, cmd, data): 57 | if sid == self.deviceSID: 58 | if 'status' in data: 59 | self.pushUpdate(model, data['status']) 60 | 61 | @property 62 | def should_poll(self): 63 | """No polling needed for a demo light.""" 64 | return False 65 | 66 | @property 67 | def unique_id(self): 68 | """Return the unique id""" 69 | return self.uniqueID 70 | 71 | @property 72 | def name(self): 73 | """Return the name of the sensor.""" 74 | return self.uniqueID 75 | 76 | @property 77 | def device_class(self): 78 | """Return the class of this sensor, from SENSOR_CLASSES.""" 79 | if self.deviceModel == 'motion': 80 | return 'motion' 81 | elif self.deviceModel == 'magnet': 82 | return 'opening' 83 | 84 | @property 85 | def is_on(self): 86 | """Return true if binary sensor is on.""" 87 | return self._state 88 | 89 | def pushUpdate(self,model,status): 90 | if model == 'magnet': 91 | if status =='open': 92 | self._state = True 93 | else: 94 | self._state = False 95 | elif model == 'motion': 96 | if status =='motion': 97 | self._state = True 98 | else: 99 | self._state = False 100 | 101 | super().update_ha_state() 102 | -------------------------------------------------------------------------------- /custom_components/sensor/aqara.py: -------------------------------------------------------------------------------- 1 | from homeassistant.const import TEMP_CELSIUS 2 | from homeassistant.helpers.entity import Entity 3 | import logging 4 | import json 5 | 6 | # DOMAIN = 'aqara' 7 | 8 | _LOGGER = logging.getLogger(__name__) 9 | SENSOR_TYPES = ['temperature', 'humidity'] 10 | 11 | 12 | def setup_platform(hass, config, add_devices, discovery_info=None): 13 | """Setup the sensor platform.""" 14 | 15 | # # get the gateway object from the hub component 16 | gateway = hass.data['AqaraGateway'] 17 | devices = gateway.sidsData 18 | 19 | sensorItems = [] 20 | for variable in SENSOR_TYPES: 21 | for device in devices: 22 | if device['model'] == 'sensor_ht': 23 | sensorItems.append(AqaraSensor(gateway, device['sid'], device['sid'], device['model'], variable,device['data'])) 24 | 25 | if len(sensorItems)> 0: 26 | add_devices(sensorItems) 27 | return True 28 | else: 29 | return False 30 | 31 | class AqaraSensor(Entity): 32 | """Representation of a Binary Sensor.""" 33 | 34 | def __init__(self, aqaraGateway,deviceName,deviceSID,deviceModel,deviceVariable,deviceData): 35 | self.gateway = aqaraGateway 36 | self.deviceName = deviceName 37 | self.deviceSID = deviceSID 38 | self.deviceModel = deviceModel 39 | self.deviceVariable = deviceVariable 40 | self.uniqueID = '{} {}'.format(deviceVariable, deviceSID) 41 | 42 | self.gateway.register(self.uniqueID, self._update_callback) 43 | 44 | if deviceVariable == 'temperature': 45 | self._state = float(deviceData['temperature'])/100 46 | elif deviceVariable == 'humidity': 47 | self._state = float(deviceData['humidity'])/100 48 | else: 49 | self._state = None 50 | 51 | def _update_callback(self, model,sid, cmd, data): 52 | if sid == self.deviceSID: 53 | self.pushUpdate(data) 54 | else: 55 | _LOGGER.error('Issue to update the sensor ',sid) 56 | 57 | @property 58 | def should_poll(self): 59 | """No polling needed for a demo light.""" 60 | return False 61 | 62 | @property 63 | def unique_id(self): 64 | """Return the unique id""" 65 | return self.uniqueID 66 | 67 | @property 68 | def name(self): 69 | """Return the name of the sensor.""" 70 | return self.uniqueID 71 | 72 | @property 73 | def state(self): 74 | """Return the state of the sensor.""" 75 | return self._state 76 | 77 | @property 78 | def unit_of_measurement(self): 79 | """Return the unit of measurement.""" 80 | if self.deviceVariable == "temperature": 81 | return TEMP_CELSIUS 82 | elif self.deviceVariable == "humidity": 83 | return '%' 84 | 85 | def pushUpdate(self,data): 86 | if self.deviceVariable == 'temperature': 87 | if 'temperature' in data and data['temperature'] != 100: 88 | self._state = float(data['temperature'])/100 89 | elif self.deviceVariable == 'humidity': 90 | if 'humidity' in data and data['humidity'] != 0: 91 | self._state = float(data['humidity'])/100 92 | super().update_ha_state() -------------------------------------------------------------------------------- /custom_components/switch/aqara.py: -------------------------------------------------------------------------------- 1 | from homeassistant.helpers.entity import Entity 2 | from homeassistant.components.switch import SwitchDevice 3 | from homeassistant.helpers.entity import ToggleEntity 4 | import logging 5 | import json 6 | import binascii 7 | iv = bytes.fromhex('17996d093d28ddb3ba695a2e6f58562e') 8 | 9 | # DOMAIN = 'aqara' 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # DEPENDENCIES = ['pyAqara'] 14 | REQUIREMENTS = ['pyCrypto'] 15 | 16 | SWITCH_TYPES = ['ONE_CLICK', 'DOUBLE_CLICK', 'LONG_PRESS'] 17 | 18 | def setup_platform(hass, config, add_devices, discovery_info=None): 19 | """Setup the sensor platform.""" 20 | # # get the gateway object from the hub component 21 | gateway = hass.data['AqaraGateway'] 22 | devices = gateway.sidsData 23 | 24 | switchItems = [] 25 | for variable in SWITCH_TYPES: 26 | for device in devices: 27 | if device['model'] == 'switch': 28 | switchItems.append(AqaraSwitchSensor(gateway, device['sid'], device['sid'], device['model'],variable)) 29 | 30 | for device in devices: 31 | if 'ctrl_neutral' in device['model']: 32 | for channel in device['data']: 33 | switchItems.append(AqaraWallSwitch(gateway, device['sid'], device['sid'], device['model'],channel)) 34 | elif device['model']=='plug': 35 | switchItems.append(PlugSwitch(gateway, device['sid'], device['sid'], device['model'])) 36 | 37 | if len(switchItems)> 0: 38 | add_devices(switchItems) 39 | return True 40 | else: 41 | return False 42 | 43 | class AqaraSwitchSensor(ToggleEntity): 44 | """Representation of a Binary Sensor.""" 45 | 46 | def __init__(self, aqaraGateway,deviceName,deviceSID,deviceModel,deviceVariable): 47 | self.gateway = aqaraGateway 48 | self.deviceName = deviceName 49 | self.deviceSID = deviceSID 50 | self.deviceModel = deviceModel 51 | self.deviceVariable = deviceVariable 52 | self.uniqueID = '{} {} {}'.format(deviceModel, deviceVariable, deviceSID) 53 | self._state = False 54 | 55 | self.gateway.register(self.deviceSID, self._update_callback) 56 | 57 | 58 | def _update_callback(self, model, sid, cmd, data): 59 | if sid == self.deviceSID: 60 | self.pushUpdate(model, data['status']) 61 | else: 62 | _LOGGER.error('Issue to update the sensor ',sid) 63 | 64 | @property 65 | def should_poll(self): 66 | """No polling needed for a demo light.""" 67 | return False 68 | 69 | @property 70 | def unique_id(self): 71 | """Return the unique id""" 72 | return self.uniqueID 73 | 74 | @property 75 | def name(self): 76 | """Return the name of the sensor.""" 77 | return self.uniqueID 78 | 79 | # @property 80 | # def sensor_class(self): 81 | # """Return the class of this sensor, from SENSOR_CLASSES.""" 82 | # return self.deviceModel 83 | 84 | @property 85 | def is_on(self): 86 | """Return true if switch is on.""" 87 | return self._state 88 | 89 | def turn_on(self): 90 | self._state = True 91 | super().update_ha_state() 92 | 93 | def turn_off(self): 94 | self._state = False 95 | super().update_ha_state() 96 | 97 | def pushUpdate(self,model,status): 98 | if self.deviceVariable == 'ONE_CLICK': 99 | if status == 'click': 100 | self._state = not self._state 101 | elif self.deviceVariable == 'DOUBLE_CLICK': 102 | if status == 'double_click': 103 | self._state = not self._state 104 | elif self.deviceVariable == 'LONG_PRESS': 105 | if status == 'long_click_press' and self._state == False: 106 | self._state = True 107 | elif status == 'long_click_release': 108 | self._state = False 109 | 110 | super().update_ha_state() 111 | 112 | def update(self): 113 | pass 114 | 115 | class AqaraWallSwitch(ToggleEntity): 116 | """Representation of a Binary Sensor.""" 117 | 118 | def __init__(self, aqaraGateway,deviceName,deviceSID,deviceModel,deviceChannel): 119 | self.gateway = aqaraGateway 120 | self.deviceName = deviceName 121 | self.deviceSID = deviceSID 122 | self.deviceModel = deviceModel 123 | self.deviceChannel = deviceChannel 124 | self.uniqueID = '{} {} {}'.format(deviceModel, deviceChannel, deviceSID) 125 | self._state = False 126 | 127 | self.gateway.register(self.deviceSID, self._update_callback) 128 | 129 | def _update_callback(self, model,sid, cmd, data): 130 | if sid == self.deviceSID: 131 | self.pushUpdate(model, data) 132 | else: 133 | _LOGGER.error('Issue to update the sensor ',sid) 134 | 135 | @property 136 | def should_poll(self): 137 | """No polling needed for a demo light.""" 138 | return True 139 | 140 | @property 141 | def unique_id(self): 142 | """Return the unique id""" 143 | return self.uniqueID 144 | 145 | @property 146 | def name(self): 147 | """Return the name of the sensor.""" 148 | return self.uniqueID 149 | 150 | # @property 151 | # def sensor_class(self): 152 | # """Return the class of this sensor, from SENSOR_CLASSES.""" 153 | # return self.deviceModel 154 | 155 | @property 156 | def is_on(self): 157 | """Return true if switch is on.""" 158 | return self._state 159 | 160 | def turn_on(self): 161 | self._turn_switch('on') 162 | 163 | def turn_off(self): 164 | self._turn_switch('off') 165 | 166 | def _turn_switch(self,state): 167 | 168 | from Crypto.Cipher import AES 169 | 170 | password = self.gateway.password 171 | if password=='': 172 | _LOGGER.error('Please add "gateway_password:" config under the "aqara: " in config.yaml ') 173 | return 174 | 175 | token = self.gateway.GATEWAY_TOKEN 176 | 177 | cipher = AES.new(password, AES.MODE_CBC, iv) 178 | key = binascii.hexlify(cipher.encrypt(token)) 179 | key = key.decode('ascii') 180 | 181 | commandDict = {"cmd":"write","model":self.deviceModel,"sid":self.deviceSID,"data":{}} 182 | commandDict["data"][self.deviceChannel] = state 183 | commandDict["data"]["key"] = key 184 | 185 | self.gateway.socketSendMsg(json.dumps(commandDict)) 186 | 187 | 188 | def pushUpdate(self,model,data): 189 | if self.deviceChannel in data: 190 | if data[self.deviceChannel] == 'on': 191 | self._state = True 192 | else: 193 | self._state = False 194 | 195 | super().update_ha_state() 196 | 197 | def update(self): 198 | self.gateway.socketSendMsg('{"cmd":"read", "sid":"' + self.deviceSID + '"}') 199 | 200 | class PlugSwitch(ToggleEntity): 201 | 202 | def __init__(self, aqaraGateway,deviceName,deviceSID,deviceModel): 203 | self.gateway = aqaraGateway 204 | self.deviceName = deviceName 205 | self.deviceSID = deviceSID 206 | self.deviceModel = deviceModel 207 | self.uniqueID = '{} {}'.format(deviceModel, deviceSID) 208 | self._state = False 209 | 210 | self.gateway.register(self.deviceSID, self._update_callback) 211 | 212 | def _update_callback(self, model, sid, cmd, data): 213 | if sid == self.deviceSID: 214 | self.pushUpdate(model, data) 215 | else: 216 | _LOGGER.error('Issue to update the sensor ',sid) 217 | 218 | @property 219 | def should_poll(self): 220 | """No polling needed for a demo light.""" 221 | return True 222 | 223 | @property 224 | def unique_id(self): 225 | """Return the unique id""" 226 | return self.uniqueID 227 | 228 | @property 229 | def name(self): 230 | """Return the name of the sensor.""" 231 | return self.uniqueID 232 | 233 | @property 234 | def icon(self): 235 | return 'mdi:power-plug' 236 | 237 | @property 238 | def is_on(self): 239 | """Return true if switch is on.""" 240 | return self._state 241 | 242 | def turn_on(self): 243 | self._turn_switch('on') 244 | 245 | def turn_off(self): 246 | self._turn_switch('off') 247 | 248 | def _turn_switch(self,state): 249 | 250 | from Crypto.Cipher import AES 251 | 252 | password = self.gateway.password 253 | if password=='': 254 | _LOGGER.error('Please add "gateway_password:" config under the "aqara: " in config.yaml ') 255 | return 256 | 257 | token = self.gateway.GATEWAY_TOKEN 258 | 259 | cipher = AES.new(password, AES.MODE_CBC, iv) 260 | key = binascii.hexlify(cipher.encrypt(token)) 261 | key = key.decode('ascii') 262 | 263 | commandDict = {"cmd":"write","model":self.deviceModel,"sid":self.deviceSID,"data":{}} 264 | commandDict["data"]["status"] = state 265 | commandDict["data"]["key"] = key 266 | 267 | self.gateway.socketSendMsg(json.dumps(commandDict)) 268 | 269 | def pushUpdate(self,model,data): 270 | if "status" in data: 271 | if data["status"] == 'on': 272 | self._state = True 273 | else: 274 | self._state = False 275 | 276 | super().update_ha_state() 277 | 278 | def update(self): 279 | self.gateway.socketSendMsg('{"cmd":"read", "sid":"' + self.deviceSID + '"}') 280 | -------------------------------------------------------------------------------- /img/IMG_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_01.png -------------------------------------------------------------------------------- /img/IMG_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_02.png -------------------------------------------------------------------------------- /img/IMG_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_04.png -------------------------------------------------------------------------------- /img/IMG_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_05.png -------------------------------------------------------------------------------- /img/IMG_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_06.png -------------------------------------------------------------------------------- /img/IMG_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassistant-aqara/62ad8641fa03efd060f5a295f17d9e89dd51462e/img/IMG_07.png --------------------------------------------------------------------------------