├── README.md ├── custom_components └── zhimi │ ├── __init__.py │ ├── entity.py │ ├── manifest.json │ └── sensor.py └── hacs.json /README.md: -------------------------------------------------------------------------------- 1 | # [https://github.com/Yonsm/ZhiMi](https://github.com/Yonsm/ZhiMi) 2 | 3 | XiaoMi Cloud Service for HomeAssistant 4 | 5 | ## 1. 安装准备 6 | 7 | 把 `zhimi` 放入 `custom_components`;也支持在 [HACS](https://hacs.xyz/) 中添加自定义库的方式安装。 8 | 9 | _部分组件依赖 [Zhi](https://github.com/Yonsm/Zhi),请一并安装。_ 10 | _依赖 [MiService](https://github.com/Yonsm/MiService),运行时自动检查安装。_ 11 | 12 | ## 2. 配置方法 13 | 14 | 参见 [我的 Home Assistant 配置](https://github.com/Yonsm/.homeassistant) 中 [configuration.yaml](https://github.com/Yonsm/.homeassistant/blob/main/configuration.yaml) 15 | 16 | ```yaml 17 | zhimi: 18 | username: !secret zhimi_username 19 | password: !secret zhimi_password 20 | ``` 21 | 22 | - `必选` `username` 小米账号 23 | - `必选` `password` 小米密码 24 | 25 | 登录后会在 `.storage` 下记录 `zhimi` 的 token 文件,删除后会自动重新登录。 26 | 27 | ## 3. 使用方式 28 | 29 | 在其它插件中使用 `get_miio_service()`,如 [ZhiMsg](https://github.com/Yonsm/ZhiMsg)。 30 | 31 | 后续如果有必要可以暴露其它服务。 32 | 33 | ## 4. 参考 34 | 35 | - [ZhiMsg](https://github.com/Yonsm/ZhiMsg) 36 | - [Yonsm.NET](https://yonsm.github.io) 37 | - [Yonsm's .homeassistant](https://github.com/Yonsm/.homeassistant) 38 | -------------------------------------------------------------------------------- /custom_components/zhimi/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from miservice import MiAccount, MiIOService 3 | from homeassistant.helpers import aiohttp_client, storage 4 | 5 | DOMAIN = 'zhimi' 6 | 7 | miio_service = MiIOService() 8 | 9 | 10 | def load_account(conf, store, sesssion): 11 | miio_service.account = MiAccount(sesssion, conf['username'], conf['password'], store) 12 | 13 | 14 | async def async_setup(hass, config): 15 | global miio_service 16 | conf = config.get(DOMAIN) 17 | store = hass.config.path(storage.STORAGE_DIR, DOMAIN) 18 | sesssion = aiohttp_client.async_get_clientsession(hass) 19 | #miio_service.account = MiAccount(sesssion, conf['username'], conf['password'], store) 20 | await hass.async_add_executor_job(load_account, conf, store, sesssion) 21 | return True 22 | -------------------------------------------------------------------------------- /custom_components/zhimi/entity.py: -------------------------------------------------------------------------------- 1 | 2 | from . import miio_service, DOMAIN 3 | from ..zhi.entity import ZhiPollEntity, ZHI_SCHEMA 4 | from homeassistant.helpers import discovery 5 | from homeassistant.const import CONF_SENSORS, CONF_MODEL, CONF_DEVICE 6 | import homeassistant.helpers.config_validation as cv 7 | import voluptuous as vol 8 | 9 | import logging 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | CONF_DID = 'did' 13 | CONF_IGNORE_STATE = 'ignore_state' 14 | 15 | ZHIMI_SCHEMA = ZHI_SCHEMA | { 16 | vol.Required(CONF_DID): cv.string, 17 | vol.Optional(CONF_MODEL): cv.string, 18 | vol.Optional(CONF_SENSORS): dict, 19 | vol.Optional(CONF_IGNORE_STATE): bool, 20 | } 21 | 22 | 23 | class ZhiMiEntity(ZhiPollEntity): 24 | 25 | def __init__(self, hass, props, conf, icon=None): 26 | super().__init__(conf, icon) 27 | # self.hass = hass 28 | self.did = conf[CONF_DID] 29 | self.ignore_state = conf.get(CONF_IGNORE_STATE) 30 | if isinstance(props, tuple): 31 | props = {e.value: e.name for s in props for e in s if not e.name.startswith('_')} 32 | if isinstance(props, dict): 33 | self.attrs = tuple(props.values()) 34 | props = tuple(props.keys()) 35 | else: 36 | self.attrs = None 37 | self.props = props 38 | 39 | if CONF_SENSORS in conf: 40 | discovery.load_platform(hass, 'sensor', DOMAIN, {CONF_SENSORS: conf[CONF_SENSORS], CONF_DEVICE: self}, {}) 41 | 42 | async def async_update(self): 43 | await super().async_update() 44 | if hasattr(self, 'sensors'): 45 | for sensor in self.sensors: 46 | sensor._attr_native_value = self.data.get(sensor.sensor_id) if self.data else None 47 | sensor.async_write_ha_state() 48 | 49 | @property 50 | def extra_state_attributes(self): 51 | return {self.attrs[i] if self.attrs else self.props[i]: self.data[self.props[i]] for i in range(len(self.props))} if self.data else None 52 | 53 | async def async_poll(self): 54 | props = self.props 55 | prop = props[0] 56 | if isinstance(prop, str) and not prop[0].isdigit(): 57 | get_props = miio_service.home_get_props 58 | else: 59 | if isinstance(prop, str): 60 | props = list(map(lambda s: tuple(map(int, s.split('-'))), props)) 61 | get_props = miio_service.miot_get_props 62 | values = await get_props(self.did, props) 63 | return {self.props[i]: values[i]/10 if props[i] == 'temp_dec' else values[i] for i in range(len(values))} 64 | 65 | async def async_control(self, prop, value=[], op=None, success=None, ignore_prop=False, alias_prop=None): 66 | has_prop = not ignore_prop and not isinstance(value, list) and self.data and prop in self.data 67 | if value is None: 68 | if has_prop: 69 | value = self.data[prop] 70 | else: 71 | _LOGGER.error("No old value for %s", prop) 72 | op and await self.async_update_status(op + '异常') 73 | return None 74 | elif not self.ignore_state and has_prop and value == self.data[prop]: 75 | _LOGGER.warn("Ignore same state value: %s", value) 76 | op and await self.async_update_status('当前已' + op) 77 | return None 78 | 79 | # op and await self.async_update_status('正在' + op) 80 | code = await self.async_action(alias_prop or prop, value) 81 | 82 | if code == 0: 83 | self.skip_poll = True 84 | if has_prop: 85 | self.data[prop] = value 86 | if success: 87 | success(prop, value) 88 | if op: 89 | await self.async_update_status(op + '成功') 90 | else: 91 | self.async_write_ha_state() 92 | return True 93 | op and await self.async_update_status(op + '错误:%s' % code) 94 | return False 95 | 96 | async def async_action(self, prop, value=[]): 97 | if isinstance(prop, str) and not prop[0].isdigit(): 98 | action = miio_service.home_set_prop 99 | else: 100 | action = (miio_service.miot_action if isinstance(value, list) else miio_service.miot_set_prop) 101 | if isinstance(prop, str): 102 | prop = tuple(map(int, prop.split('-'))) 103 | return await action(self.did, prop, value) 104 | 105 | async def async_update_status(self, status): 106 | _LOGGER.debug("async_update_status: %s", status) 107 | self.async_write_ha_state() 108 | -------------------------------------------------------------------------------- /custom_components/zhimi/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "zhimi", 3 | "name": "Zhi XiaoMi Cloud Service", 4 | "documentation": "https://github.com/Yonsm/ZhiMi", 5 | "version": "1.0.1", 6 | "requirements": ["miservice>=2.0.1"], 7 | "dependencies": [], 8 | "codeowners": ["@Yonsm"] 9 | } 10 | -------------------------------------------------------------------------------- /custom_components/zhimi/sensor.py: -------------------------------------------------------------------------------- 1 | from homeassistant.components.sensor import SensorEntity 2 | from homeassistant.const import CONF_SENSORS, CONF_DEVICE 3 | 4 | 5 | async def async_setup_platform(hass, conf, async_add_entities, discovery_info=None): 6 | sensors = [] 7 | for k, v in discovery_info[CONF_SENSORS].items(): 8 | sensor = SensorEntity() 9 | sensor.sensor_id = k 10 | sensor._attr_name = v 11 | sensor._attr_should_poll = False 12 | device_class = sensor._attr_device_class = {'temp_dec': 'temperature', 'aqi': 'pm25', 'co2': 'carbon_dioxide'}.get(k, k) 13 | sensor._attr_native_unit_of_measurement = {'temperature': '°C', 'humidity': '%', 'pm25': 'µg/m³', 'carbon_dioxide': 'ppm'}.get(device_class) 14 | sensors.append(sensor) 15 | discovery_info[CONF_DEVICE].sensors = sensors 16 | async_add_entities(sensors, True) 17 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Zhi XiaoMi Cloud Service", 3 | "domains": ["zhimi"], 4 | "render_readme": true 5 | } 6 | --------------------------------------------------------------------------------