├── README.md ├── custom_components └── miheater │ ├── __init__.py │ ├── climate.py │ └── manifest.json └── hacs.json /README.md: -------------------------------------------------------------------------------- 1 | 9 | # Smartmi Smart Heater 10 | 11 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) 12 | 13 | This is a custom component for home assistant to integrate the Smartmi smart heater. 14 | 15 | ![Smartmi_smart_heater](https://sc01.alicdn.com/kf/H0beecf680bd94ab284ceab2476b0b01bP/231348130/H0beecf680bd94ab284ceab2476b0b01bP.jpg) 16 | 17 | Please follow the instructions on [Retrieving the Access Token](https://home-assistant.io/components/xiaomi/#retrieving-the-access-token) to get the API token to use in the configuration.yaml file. 18 | 19 | Credits: Thanks to [Rytilahti](https://github.com/rytilahti/python-miio) for all the work. 20 | 21 | ## Features 22 | 23 | ### viomi.health_pot.v1 24 | 25 | * Sensors 26 | - hvac_modes: heat,off 27 | - min_temp 28 | - max_temp 29 | - target_temp_step 30 | - current_temperature 31 | - temperature 32 | - curr_tempe 33 | - power 34 | - humidity 35 | - target_temperature 36 | * Chart 37 | - Temperature History 38 | * Services 39 | - set_hvac_mode 40 | - set_temperature 41 | - xiaomi_heater_set_buzzer 42 | - xiaomi_heater_set_brightness 43 | - xiaomi_heater_set_poweroff_time 44 | 45 | 46 | ## Setup 47 | 48 | ```yaml 49 | # configuration.yaml 50 | 51 | climate: 52 | - platform: miheater 53 | host: 192.168.1.13 54 | token: a9bd32552dc9bd4e156954c20ddbcb38 55 | name: 取暖器 56 | 57 | ``` 58 | 59 | Configuration variables: 60 | - **host** (*Required*): The IP of your cooker. 61 | - **token** (*Required*): The API token of your cooker. 62 | - **name** (*Optional*): The name of your cooker. 63 | 64 | ## Platform services 65 | 66 | #### Service `climate.set_hvac_mode` 67 | 68 | Specify the heater mode. 69 | 70 | | Service data attribute | Optional | Description | 71 | |---------------------------|----------|----------------------------------------------------------------------| 72 | | `entity_id` | yes | Only act on a specific heater. | 73 | | `hvac_mode` | yes | Specify the heater mode (heat/off). | 74 | 75 | #### Service `climate.set_temperature` 76 | 77 | Set the temperature of heater. 78 | 79 | | Service data attribute | Optional | Description | 80 | |---------------------------|----------|----------------------------------------------------------------------| 81 | | `entity_id` | yes | Only act on a specific heater. | 82 | | `temperature` | yes | Set the temperature of heater. | 83 | 84 | ## Update 85 | 86 | #### `climate.xiaomi_heater_set_buzzer` 87 | 88 | 设置蜂鸣器开关 89 | 90 | | Service data attribute | Optional | Description | 91 | |---------------------------|----------|----------------------------------------------------------------------| 92 | | `entity_id` | yes | Only act on a specific heater. | 93 | | `buzzer` | yes | 设置蜂鸣器开关 off 或者 on | 94 | 95 | 96 | #### `climate.xiaomi_heater_set_brightness` 97 | 98 | 设置面板亮度 99 | 100 | | Service data attribute | Optional | Description | 101 | |---------------------------|----------|----------------------------------------------------------------------| 102 | | `entity_id` | yes | Only act on a specific heater. | 103 | | `brightness` | yes | 设置面板亮度,分别可以是0,1,2 | 104 | 105 | #### `climate.xiaomi_heater_set_poweroff_time` 106 | 107 | 定时关闭 108 | 109 | | Service data attribute | Optional | Description | 110 | |---------------------------|----------|----------------------------------------------------------------------| 111 | | `entity_id` | yes | Only act on a specific heater. | 112 | | `buzzer` | yes | 延迟关闭的时间, 单位为分钟 | 113 | 114 | #### `climate.xiaomi_heater_set_child_lock` 115 | 116 | 设置童锁 117 | 118 | | Service data attribute | Optional | Description | 119 | |---------------------------|----------|----------------------------------------------------------------------| 120 | | `entity_id` | yes | Only act on a specific heater. | 121 | | `buzzer` | yes | 设置儿童锁开关 off 或者 on | 122 | -------------------------------------------------------------------------------- /custom_components/miheater/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @Author : fineemb 3 | @Github : https://github.com/fineemb 4 | @Description : 5 | @Date : 2019-10-25 00:52:13 6 | @LastEditors : fineemb 7 | @LastEditTime : 2019-10-25 19:13:24 8 | ''' 9 | -------------------------------------------------------------------------------- /custom_components/miheater/climate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Xiaomi wifi-enabled home heaters via miio. 3 | author: sunfang1cn@gmail.com 4 | """ 5 | import logging 6 | import enum 7 | import voluptuous as vol 8 | import asyncio 9 | 10 | from homeassistant.components.climate import ClimateEntity, PLATFORM_SCHEMA 11 | from homeassistant.components.climate.const import ( 12 | DOMAIN, ATTR_HVAC_MODE, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF, 13 | SUPPORT_TARGET_TEMPERATURE) 14 | from homeassistant.const import ( 15 | ATTR_TEMPERATURE, CONF_HOST, ATTR_ENTITY_ID, CONF_NAME, CONF_TOKEN, 16 | TEMP_CELSIUS) 17 | from homeassistant.helpers import config_validation as cv 18 | from homeassistant.helpers.entity import generate_entity_id 19 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 20 | from homeassistant.exceptions import PlatformNotReady 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | REQUIREMENTS = ['python-miio>=0.5.11'] 25 | SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE) 26 | DATA_KEY = 'climate.xiaomi_miio_heater' 27 | 28 | SERVICE_SET_BUZZER = 'xiaomi_heater_set_buzzer' 29 | SERVICE_SET_BRIGHTNESS = 'xiaomi_heater_set_brightness' 30 | SERVICE_SET_POWEROFF_TIME = 'xiaomi_heater_set_poweroff_time' 31 | SERVICE_SET_CHILD_LOCK = 'xiaomi_heater_set_child_lock' 32 | 33 | CONF_BUZZER = 'buzzer' 34 | CONF_BRIGHTNESS = 'brightness' 35 | CONF_POWEROFF_TIME = 'poweroff_time' 36 | CONF_CHILD_LOCK = 'lock' 37 | 38 | MIN_TEMP = 16 39 | MAX_TEMP = 32 40 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 41 | vol.Required(CONF_HOST): cv.string, 42 | vol.Required(CONF_NAME): cv.string, 43 | vol.Required(CONF_TOKEN): cv.string, 44 | }) 45 | 46 | SERVICE_SCHEMA = vol.Schema({ 47 | vol.Required(ATTR_ENTITY_ID): cv.entity_ids, 48 | }) 49 | 50 | SERVICE_SCHEMA_SET_BUZZER = SERVICE_SCHEMA.extend({ 51 | vol.Required(CONF_BUZZER): vol.All(vol.Coerce(str), vol.Clamp('off', 'on')) 52 | }) 53 | SERVICE_SCHEMA_SET_BRIGHTNESS = SERVICE_SCHEMA.extend({ 54 | vol.Required(CONF_BRIGHTNESS): vol.All(vol.Coerce(int), vol.Range(min=0, max=2)), 55 | }) 56 | SERVICE_SCHEMA_SET_POWEROFF_TIME = SERVICE_SCHEMA.extend({ 57 | vol.Required(CONF_POWEROFF_TIME): vol.All(vol.Coerce(int), vol.Range(min=0, max=28800)), 58 | }) 59 | SERVICE_SCHEMA_SET_CHILD_LOCK = SERVICE_SCHEMA.extend({ 60 | vol.Required(CONF_CHILD_LOCK): vol.All(vol.Coerce(str), vol.Clamp('off', 'on')) 61 | }) 62 | 63 | 64 | SERVICE_TO_METHOD = { 65 | SERVICE_SET_BUZZER: {'method': 'async_set_buzzer', 66 | 'schema': SERVICE_SCHEMA_SET_BUZZER}, 67 | SERVICE_SET_BRIGHTNESS: {'method': 'async_set_brightness', 68 | 'schema': SERVICE_SCHEMA_SET_BRIGHTNESS}, 69 | SERVICE_SET_POWEROFF_TIME: {'method': 'async_set_poweroff_time', 70 | 'schema': SERVICE_SCHEMA_SET_POWEROFF_TIME}, 71 | SERVICE_SET_CHILD_LOCK: {'method': 'async_set_child_lock', 72 | 'schema': SERVICE_SCHEMA_SET_CHILD_LOCK}, 73 | } 74 | 75 | def setup_platform(hass, config, async_add_devices, discovery_info=None): 76 | """Perform the setup for Xiaomi heaters.""" 77 | from miio import Device, DeviceException 78 | if DATA_KEY not in hass.data: 79 | hass.data[DATA_KEY] = {} 80 | 81 | host = config.get(CONF_HOST) 82 | name = config.get(CONF_NAME) 83 | token = config.get(CONF_TOKEN) 84 | 85 | _LOGGER.info("Initializing Xiaomi heaters with host %s (token %s...)", host, token[:5]) 86 | 87 | try: 88 | device = Device(host, token) 89 | 90 | device_info = device.info() 91 | model = device_info.model 92 | unique_id = "{}-{}".format(model, device_info.mac_address) 93 | _LOGGER.info("%s %s %s detected", 94 | model, 95 | device_info.firmware_version, 96 | device_info.hardware_version) 97 | except DeviceException: 98 | _LOGGER.exception('Fail to setup Xiaomi heater') 99 | raise PlatformNotReady 100 | miHeater = MiHeater(device, name, unique_id, hass) 101 | hass.data[DATA_KEY][host] = miHeater 102 | async_add_devices([miHeater], update_before_add=True) 103 | 104 | async def async_service_handler(service): 105 | """Map services to methods on XiaomiAirConditioningCompanion.""" 106 | method = SERVICE_TO_METHOD.get(service.service) 107 | params = {key: value for key, value in service.data.items() 108 | if key != ATTR_ENTITY_ID} 109 | entity_ids = service.data.get(ATTR_ENTITY_ID) 110 | if entity_ids: 111 | devices = [device for device in hass.data[DATA_KEY].values() if 112 | device.entity_id in entity_ids] 113 | else: 114 | devices = hass.data[DATA_KEY].values() 115 | 116 | update_tasks = [] 117 | for device in devices: 118 | if not hasattr(device, method['method']): 119 | continue 120 | await getattr(device, method['method'])(**params) 121 | update_tasks.append(device.async_update_ha_state(True)) 122 | 123 | if update_tasks: 124 | await asyncio.wait(update_tasks, loop=hass.loop) 125 | 126 | for service in SERVICE_TO_METHOD: 127 | schema = SERVICE_TO_METHOD[service].get('schema', SERVICE_SCHEMA) 128 | hass.services.async_register( 129 | DOMAIN, service, async_service_handler, schema=schema) 130 | 131 | class OperationMode(enum.Enum): 132 | Heat = 'heat' 133 | Off = 'off' 134 | 135 | class MiHeater(ClimateEntity): 136 | from miio import DeviceException 137 | 138 | """Representation of a MiHeater device.""" 139 | 140 | def __init__(self, device, name, unique_id, _hass): 141 | """Initialize the heater.""" 142 | self._device = device 143 | self._name = name 144 | self._state_attrs = {} 145 | self._target_temperature = 0 146 | self._current_temperature = 0 147 | self._power = None 148 | self._poweroff_time = None 149 | self._buzzer = None 150 | self._brightness = None 151 | self._child_lock = None 152 | self._hvac_mode = None 153 | self.entity_id = generate_entity_id('climate.{}', unique_id, hass=_hass) 154 | 155 | @property 156 | def name(self): 157 | """Return the name of the device.""" 158 | return self._name 159 | 160 | @property 161 | def state(self) -> str: 162 | """Return the current state.""" 163 | return self.hvac_mode 164 | 165 | @property 166 | def supported_features(self): 167 | """Return the list of supported features.""" 168 | return SUPPORT_FLAGS 169 | @property 170 | def temperature_unit(self): 171 | """Return the unit of measurement which this thermostat uses.""" 172 | return TEMP_CELSIUS 173 | @property 174 | def target_temperature(self): 175 | """Return the temperature we try to reach.""" 176 | return self._target_temperature 177 | 178 | @property 179 | def current_temperature(self): 180 | """Return the current temperature.""" 181 | return self._current_temperature 182 | 183 | @property 184 | def hvac_modes(self): 185 | """Return the list of available hvac modes.""" 186 | return [mode.value for mode in OperationMode] 187 | 188 | @property 189 | def hvac_mode(self): 190 | """Return hvac mode ie. heat, cool, fan only.""" 191 | return self._hvac_mode 192 | 193 | @property 194 | def target_temperature_step(self): 195 | """Return the supported step of target temperature.""" 196 | return 1 197 | 198 | @asyncio.coroutine 199 | def async_update(self): 200 | """Update the state of this device.""" 201 | try: 202 | data = {} 203 | power = self._device.send('get_prop', ['power'])[0] 204 | humidity = self._device.send('get_prop', ['relative_humidity'])[0] 205 | target_temperature = self._device.send('get_prop', ['target_temperature'])[0] 206 | current_temperature = self._device.send('get_prop', ['temperature'])[0] 207 | poweroff_time = self._device.send('get_prop', ['poweroff_time'])[0] 208 | buzzer = self._device.send('get_prop', ['buzzer'])[0] 209 | brightness = self._device.send('get_prop', ['brightness'])[0] 210 | child_lock = self._device.send('get_prop', ['child_lock'])[0] 211 | if power == 'off': 212 | self._hvac_mode = 'off' 213 | else: 214 | self._hvac_mode = "heat" 215 | self._poweroff_time = None 216 | self._buzzer = None 217 | self._brightness = None 218 | self._child_lock = None 219 | self._target_temperature = target_temperature 220 | self._current_temperature = current_temperature 221 | self._power = power != "off" 222 | self._state_attrs.update({ 223 | ATTR_HVAC_MODE: power if power == "off" else "heat", 224 | ATTR_TEMPERATURE:target_temperature, 225 | "power": power, 226 | "humidity": humidity, 227 | "poweroff_time": poweroff_time, 228 | "buzzer": buzzer, 229 | "brightness": brightness, 230 | "child_lock": child_lock, 231 | "current_temperature":current_temperature, 232 | "temperature":target_temperature 233 | }) 234 | except DeviceException: 235 | _LOGGER.exception('Fail to get_prop from Xiaomi heater') 236 | raise PlatformNotReady 237 | 238 | @property 239 | def device_state_attributes(self): 240 | """Return the state attributes of the device.""" 241 | return self._state_attrs 242 | 243 | @property 244 | def min_temp(self): 245 | """Return the minimum temperature.""" 246 | return MIN_TEMP 247 | 248 | @property 249 | def max_temp(self): 250 | """Return the maximum temperature.""" 251 | return MAX_TEMP 252 | 253 | @property 254 | def current_operation(self): 255 | """Return current operation.""" 256 | return OperationMode.Off.value if self._power == 'off' else OperationMode.Heat.value 257 | 258 | @property 259 | def operation_list(self): 260 | """List of available operation modes.""" 261 | return [HVAC_MODE_HEAT, HVAC_MODE_OFF] 262 | 263 | @asyncio.coroutine 264 | def async_set_temperature(self, **kwargs): 265 | """Set new target temperature.""" 266 | temperature = kwargs.get(ATTR_TEMPERATURE) 267 | if temperature is None: 268 | return 269 | self._device.send('set_target_temperature', [int(temperature)]) 270 | 271 | @asyncio.coroutine 272 | def async_set_brightness(self, **kwargs): 273 | """Set new led brightness.""" 274 | brightness = kwargs.get(CONF_BRIGHTNESS) 275 | if brightness is None: 276 | return 277 | self._device.send('set_brightness', [int(brightness)]) 278 | 279 | @asyncio.coroutine 280 | def async_set_poweroff_time(self, **kwargs): 281 | """Set new led brightness.""" 282 | poweroff_time = kwargs.get(CONF_POWEROFF_TIME) 283 | if poweroff_time is None: 284 | return 285 | self._device.send('set_poweroff_time', [int(poweroff_time)]) 286 | 287 | @asyncio.coroutine 288 | def async_set_child_lock(self, **kwargs): 289 | """Set new led brightness.""" 290 | child_lock = kwargs.get(CONF_CHILD_LOCK) 291 | if child_lock is None: 292 | return 293 | self._device.send('set_child_lock', [str(child_lock)]) 294 | 295 | @asyncio.coroutine 296 | def async_set_buzzer(self, **kwargs): 297 | """Set new led brightness.""" 298 | buzzer = kwargs.get(CONF_BUZZER) 299 | if buzzer is None: 300 | return 301 | self._device.send('set_buzzer', [str(buzzer)]) 302 | 303 | @asyncio.coroutine 304 | def async_set_hvac_mode(self, hvac_mode): 305 | """Set new target hvac mode.""" 306 | if hvac_mode == OperationMode.Heat.value: 307 | result = self._device.send('set_power', ['on']) 308 | # if result[0] == 'ok': 309 | # self.async_update() 310 | # self.async_turn_on() 311 | elif hvac_mode == OperationMode.Off.value: 312 | result = self._device.send('set_power', ['off']) 313 | # if result[0] == 'ok': 314 | # self.async_update() 315 | # self.async_turn_off() 316 | else: 317 | _LOGGER.error("Unrecognized operation mode: %s", hvac_mode) 318 | 319 | -------------------------------------------------------------------------------- /custom_components/miheater/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "miheater", 3 | "name": "Smartmi Smart Heater", 4 | "version": "1.0.0", 5 | "documentation": "https://github.com/fineemb/Smartmi-smart-heater", 6 | "requirements": [ 7 | "construct==2.9.45", 8 | "python-miio==0.5.4" 9 | ], 10 | "dependencies": [], 11 | "codeowners": [ 12 | "@fineemb" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Smartmi smart heater", 3 | "domains": ["climate"], 4 | "render_readme": true, 5 | "homeassistant": "0.99.9", 6 | "country": ["CN"] 7 | } 8 | --------------------------------------------------------------------------------