├── hacs.json ├── custom_components └── konke │ ├── manifest.json │ ├── light.py │ ├── remote.py │ └── switch.py ├── info.md ├── README.md └── LICENSE /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Konke", 3 | "country": "CN", 4 | "render_readme": false, 5 | "domains": ["sensor","swtich"] 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/konke/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "konke", 3 | "name": "控客小K", 4 | "version": "1.0", 5 | "documentation": "https://github.com/jedmeng/homeassistant-konke", 6 | "dependencies": [], 7 | "codeowners": ["@jedmeng"], 8 | "requirements": ["pykonkeio==2.1.7"] 9 | } 10 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | 配置例子: 2 | 3 | ``` 4 | switch: 5 | - platform: konke 6 | name: "电视柜电源" 7 | host: 10.10.10.241 8 | model: minik 9 | - platform: konke 10 | name: "IR红外插座" 11 | host: 10.10.10.243 12 | model: minik 13 | ``` 14 | 15 | 更多教程 :https://sumju.net 16 | 电报 群 :https://t.me/joinchat/J26zVFGMhWWB1sBTFvcjaA 17 | 电报频道 :https://t.me/itcommander 18 | Twitter :https://twitter.com/itcommander2 19 | Facebook. :https://www.facebook.com/itcommander.itcommander.1 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 修改原插件为新版本Home Assistant支持,目前测试支持0.103版本。 3 | 4 | [Home Assistant](https://www.home-assistant.io/) component of [Konke](http://www.ikonke.com/) devices 5 | 6 | # Supported Devices 7 | 8 | - Mini K 9 | 10 | ![Mini K](https://p5.ssl.qhimg.com/dm/300_300_/t01763cbb0d461968a5.png) 11 | - Mini Pro 12 | 13 | ![Mini Pro](https://p2.ssl.qhimg.com/dm/300_300_/t01da45d0484178dfab.jpg) 14 | - Smart Plug K(untested) 15 | 16 | ![K](https://p1.ssl.qhimg.com/dm/300_300_/t016c9c239d8d71fb78.jpg) 17 | - K2 Pro(untested) 18 | 19 | ![K2](https://p1.ssl.qhimg.com/dm/300_300_/t019a7103eb99573480.jpg) 20 | 21 | - (cnct) intelliPLUG (Mini K us version, untested) 22 | 23 | ![intelliPLUG](https://p5.ssl.qhimg.com/dm/300_300_/t0166baaec86aa83a4b.jpg) 24 | 25 | # Install 26 | copy the `custom_components` to your home-assistant config directory. 27 | 28 | # config 29 | Add the following to your configuration.yaml file: 30 | ```yaml 31 | switch: 32 | - platform: konke 33 | name: switch_1 34 | host: 192.168.0.101 35 | - platform: konke 36 | name: switch_2 37 | host: 192.168.0.102 38 | ``` 39 | 40 | CONFIGURATION VARIABLES: 41 | 42 | - name 43 | (string)(Optional)The display name of the device 44 | 45 | - host 46 | (string)(Required)The host/IP address of the device. 47 | 48 | 遥控使用方法: 49 | 控客的遥控器是不支持直接输入遥控编码的,只能通过学习添加遥控器。 50 | 51 | - 添加遥控: 52 | 进入service界面,选择remote.koneke_ir_learn_command或remote.koneke_rf_learn_command。 53 | ```yaml 54 | { 55 | "entity_id": 【设备的entity_id】, 56 | "slot": 【命令id,取值范围1000-99999】, 57 | "timeout": 【超时时长,默认10s】 58 | } 59 | ``` 60 | 注意solt参数是int格式,周围不能带引号,timeout参数不带单位,否则命令会不生效。 61 | 学习后会在主界面显示通知信息,提示学习的成功或者失败。 62 | 63 | - 使用遥控: 64 | 调用remote.send_command这个service,data: 65 | ```yaml 66 | { 67 | "entity_id": 【设备的entity_id】, 68 | "command": 【遥控类型ir或rf】_【命令id,取值范围1000-99999】, 69 | "num_repeats": 【发送次数,默认1】, 70 | "delay_secs": 【两次发射间的延时,默认0.4s】 71 | } 72 | ``` 73 | 使用示例: 74 | 75 | configuration.yaml中添加: 76 | ```yaml 77 | remote: 78 | - platform: konke 79 | name: k_ir_remote 80 | model: minik 81 | host: 192.168.2.162 82 | hidden: false 83 | type: ir 84 | ``` 85 | 学习遥控: 86 | ```yaml 87 | service: remote.koneke_ir_learn 88 | data: 89 | entity_id: remote.k_ir_remote 90 | slot: 1001 91 | timeout: 10 92 | ``` 93 | 使用遥控: 94 | ```yaml 95 | service: remote.send_command 96 | data: 97 | entity_id: remote.k_ir_remote 98 | command: ir_1001 99 | num_repeats: 1 100 | delay_secs: 0.4 101 | ``` 102 | -------------------------------------------------------------------------------- /custom_components/konke/light.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for the Opple light. 3 | 4 | For more details about this platform, please refer to the documentation at 5 | https://home-assistant.io/components/light.opple/ 6 | """ 7 | 8 | import logging 9 | 10 | import voluptuous as vol 11 | 12 | from homeassistant.components.light import ( 13 | ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, 14 | SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) 15 | from homeassistant.const import CONF_HOST, CONF_NAME 16 | import homeassistant.helpers.config_validation as cv 17 | from homeassistant.util.color import \ 18 | color_temperature_kelvin_to_mired as kelvin_to_mired 19 | from homeassistant.util.color import \ 20 | color_temperature_mired_to_kelvin as mired_to_kelvin 21 | from homeassistant.util.color import \ 22 | color_hs_to_RGB as hs_to_RGB 23 | from homeassistant.util.color import \ 24 | color_RGB_to_hs as RGB_to_hs 25 | 26 | REQUIREMENTS = ['pykonkeio>=2.1.7'] 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | DEFAULT_NAME = "Konke Light" 31 | 32 | CONF_MODEL = 'model' 33 | MODEL_KLIGHT = 'klight' 34 | MODEL_KBULB = 'kbulb' 35 | MODEL_K2_LIGHT = 'k2_light' 36 | 37 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 38 | vol.Required(CONF_HOST): cv.string, 39 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 40 | vol.Required(CONF_MODEL): vol.In(( 41 | MODEL_KLIGHT, 42 | MODEL_KBULB, 43 | MODEL_K2_LIGHT)), 44 | }) 45 | 46 | KBLUB_MIN_KELVIN = 2700 47 | KBLUB_MAX_KELVIN = 6493 48 | 49 | 50 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 51 | """Set up Konke light platform.""" 52 | name = config[CONF_NAME] 53 | host = config[CONF_HOST] 54 | model = config[CONF_MODEL].lower() 55 | 56 | if model == MODEL_KLIGHT: 57 | from pykonkeio.device import KLight 58 | device = KLight(host) 59 | elif model == MODEL_KBULB: 60 | from pykonkeio.device import KBulb 61 | device = KBulb(host) 62 | else: 63 | from pykonkeio.device import K2 64 | device = K2(host) 65 | 66 | entity = KonkeLight(device, name, model) 67 | async_add_entities([entity]) 68 | 69 | _LOGGER.debug("Init %s %s %s", model, host, entity.unique_id) 70 | 71 | 72 | class KonkeLight(Light): 73 | """Konke light device.""" 74 | 75 | def __init__(self, device, name: str, model: str): 76 | """Initialize an Konke light.""" 77 | self._name = name 78 | self._model = model 79 | self._device = device 80 | 81 | @property 82 | def available(self) -> bool: 83 | """Return True if light is available.""" 84 | return self._device.is_online 85 | 86 | @property 87 | def unique_id(self) -> str: 88 | """Return unique ID for light.""" 89 | if self._device.mac and self._model == MODEL_K2_LIGHT: 90 | return self._device.mac + ':light' 91 | else: 92 | return self._device.mac 93 | 94 | @property 95 | def name(self) -> str: 96 | """Return the display name of this light.""" 97 | return self._name 98 | 99 | @property 100 | def is_on(self) -> bool: 101 | """Return true if light is on.""" 102 | if self._model == MODEL_K2_LIGHT: 103 | return self._device.light_status == 'open' 104 | else: 105 | return self._device.status == 'open' 106 | 107 | @property 108 | def brightness(self) -> int: 109 | """Return the brightness of the light.""" 110 | return self._device.brightness / 100 * 255 111 | 112 | @property 113 | def hs_color(self): 114 | """Return the hs color value.""" 115 | return RGB_to_hs(*self._device.color) 116 | 117 | @property 118 | def color_temp(self) -> float: 119 | """Return the color temperature of this light.""" 120 | return kelvin_to_mired(self._device.ct) 121 | 122 | @property 123 | def min_mireds(self) -> float: 124 | """Return minimum supported color temperature.""" 125 | return kelvin_to_mired(KBLUB_MAX_KELVIN) 126 | 127 | @property 128 | def max_mireds(self) -> float: 129 | """Return maximum supported color temperature.""" 130 | return kelvin_to_mired(KBLUB_MIN_KELVIN) 131 | 132 | @property 133 | def supported_features(self) -> int: 134 | """Flag supported features.""" 135 | if self._model == MODEL_KBULB: 136 | return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP 137 | elif self._model == MODEL_KLIGHT: 138 | return SUPPORT_BRIGHTNESS | SUPPORT_COLOR 139 | else: 140 | return 0 141 | 142 | async def async_turn_on(self, **kwargs) -> None: 143 | """Instruct the light to turn on.""" 144 | _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) 145 | if not self.is_on: 146 | if self._model == MODEL_K2_LIGHT: 147 | await self._device.turn_on_light() 148 | else: 149 | await self._device.turn_on() 150 | 151 | if ATTR_BRIGHTNESS in kwargs and \ 152 | self.brightness != kwargs[ATTR_BRIGHTNESS]: 153 | await self._device.set_brightness(int(round(kwargs[ATTR_BRIGHTNESS] * 100 / 255))) 154 | 155 | if ATTR_COLOR_TEMP in kwargs and \ 156 | self.color_temp != kwargs[ATTR_COLOR_TEMP]: 157 | await self._device.set_ct(mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])) 158 | 159 | if ATTR_HS_COLOR in kwargs and \ 160 | self.hs_color != kwargs[ATTR_HS_COLOR]: 161 | hs_color = kwargs[ATTR_HS_COLOR] 162 | rgb_color = hs_to_RGB(*hs_color) 163 | await self._device.set_color(*rgb_color) 164 | 165 | async def async_turn_off(self, **kwargs) -> None: 166 | """Instruct the light to turn off.""" 167 | 168 | if self._model == MODEL_K2_LIGHT: 169 | await self._device.turn_off_light() 170 | else: 171 | await self._device.turn_off() 172 | _LOGGER.debug("Turn off light %s", self._device.ip) 173 | 174 | async def async_update(self) -> None: 175 | """Synchronize state with light.""" 176 | from pykonkeio.error import DeviceOffline 177 | prev_available = self.available 178 | try: 179 | await self._device.update(type='light') 180 | except DeviceOffline: 181 | if prev_available: 182 | _LOGGER.warning('Device is offline %s', self.entity_id) 183 | 184 | -------------------------------------------------------------------------------- /custom_components/konke/remote.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for the Konke Remote Device(K2 with IR/RF module or MiniK Pro). 3 | 4 | For more details about this platform, please refer to the documentation 5 | https://home-assistant.io/components/remote.xiaomi_miio/ 6 | """ 7 | import asyncio 8 | import logging 9 | 10 | import voluptuous as vol 11 | 12 | from homeassistant.components.remote import ( 13 | PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS, 14 | DEFAULT_DELAY_SECS, RemoteEntity) 15 | from homeassistant.const import ( 16 | CONF_NAME, CONF_HOST, CONF_TIMEOUT, CONF_TYPE, 17 | ATTR_ENTITY_ID, CONF_COMMAND) 18 | import homeassistant.helpers.config_validation as cv 19 | 20 | REQUIREMENTS = ['pykonkeio>=2.1.7'] 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | SERVICE_IR_LEARN = 'koneke_ir_learn' 25 | SERVICE_RF_LEARN = 'koneke_rf_learn' 26 | DATA_KEY = 'remote.konke_remote' 27 | SOLT_RANGE = {"min": 1000, "max": 999999} 28 | 29 | CONF_HIDDEN = 'hidden' 30 | CONF_MODEL = 'model' 31 | CONF_SLOT = 'slot' 32 | MODEL_K2 = ['k2', 'k2 pro'] 33 | MODEL_MINIK = ['minik', 'minik pro'] 34 | TYPE_IR = 'ir' 35 | TYPE_RF = 'rf' 36 | 37 | DEFAULT_NAME = 'konke_remote' 38 | DEFAULT_TIMEOUT = 10 39 | DEFAULT_SLOT = 1001 40 | 41 | ENTITIES = [] 42 | 43 | LEARN_COMMAND_SCHEMA = vol.Schema({ 44 | vol.Required(ATTR_ENTITY_ID): vol.All(str), 45 | vol.Required(CONF_SLOT): vol.All(int, vol.Range(**SOLT_RANGE)), 46 | vol.Optional(CONF_TIMEOUT, default=10): vol.All(int, vol.Range(min=0)), 47 | }) 48 | 49 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 50 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 51 | vol.Required(CONF_HOST): cv.string, 52 | vol.Optional(CONF_TYPE, default=TYPE_IR): vol.In((TYPE_IR, TYPE_RF)), 53 | vol.Optional(CONF_HIDDEN, default=True): cv.boolean, 54 | vol.Required(CONF_MODEL): vol.In(MODEL_K2 + MODEL_MINIK), 55 | }, extra=vol.ALLOW_EXTRA) 56 | 57 | 58 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 59 | """Set up the Konke Remote platform.""" 60 | from pykonkeio.manager import get_device 61 | 62 | name = config[CONF_NAME] 63 | host = config[CONF_HOST] 64 | model = config[CONF_MODEL] 65 | hidden = config[CONF_HIDDEN] 66 | remote_type = config[CONF_TYPE] 67 | 68 | entity = KonkeRemote(get_device(host, model), name, remote_type, hidden) 69 | async_add_entities([entity]) 70 | 71 | ENTITIES.append(entity) 72 | 73 | async def async_service_handler(service): 74 | """Handle a learn command.""" 75 | if service.service not in (SERVICE_IR_LEARN, SERVICE_RF_LEARN): 76 | _LOGGER.error("We should not handle service: %s", service.service) 77 | return 78 | 79 | entity_id = service.data[ATTR_ENTITY_ID] 80 | 81 | entities = [_entity for _entity in ENTITIES if _entity.entity_id in entity_id] 82 | 83 | if len(entities) == 0: 84 | _LOGGER.error("Entity_id: '%s' not found", entity_id) 85 | return 86 | 87 | slot = service.data.get(CONF_SLOT) 88 | timeout = service.data.get(CONF_TIMEOUT) 89 | 90 | log_title = 'Konke %s Remote' % entity.type 91 | log_message = 'Start learning %s remote, please press any key you want to learn on the remote.' % entity.type 92 | hass.components.persistent_notification.async_create(log_message, log_title, notification_id=entity_id) 93 | 94 | _LOGGER.debug('Start learning %s remote on slot %s: %s', entity.type, slot, entity_id) 95 | 96 | result = await hass.async_add_job(entity.async_learn, slot, timeout) 97 | 98 | if result: 99 | log_message = 'Learn %s remote success on slot %s' % (entity.type, slot) 100 | hass.components.persistent_notification.async_create(log_message, log_title, notification_id=entity_id) 101 | _LOGGER.debug('Learn %s remote success on slot %s: %s', entity.type, slot, entity_id) 102 | else: 103 | log_message = 'Learn %s remote failed on slot %s' % (entity.type, slot) 104 | hass.components.persistent_notification.async_create(log_message, log_title, notification_id=entity_id) 105 | _LOGGER.debug('Learn %s remote failed on slot %s: %s', entity.type, slot, entity_id) 106 | 107 | if remote_type == TYPE_IR: 108 | hass.services.async_register(DOMAIN, SERVICE_IR_LEARN, async_service_handler, schema=LEARN_COMMAND_SCHEMA) 109 | elif remote_type == TYPE_RF: 110 | hass.services.async_register(DOMAIN, SERVICE_RF_LEARN, async_service_handler, schema=LEARN_COMMAND_SCHEMA) 111 | 112 | 113 | class KonkeRemote(RemoteEntity): 114 | """Representation of a Xiaomi Miio Remote device.""" 115 | 116 | def __init__(self, device, name, remote_type, hidden): 117 | """Initialize the remote.""" 118 | self._name = name 119 | self._device = device 120 | self._is_hidden = hidden 121 | self._type = remote_type 122 | self._state = False 123 | 124 | @property 125 | def unique_id(self) -> str: 126 | """Return an unique ID.""" 127 | return '%s:%s' % (self._device.mac, self.type) 128 | 129 | @property 130 | def name(self) -> str: 131 | """Return the name of the remote.""" 132 | return self._name 133 | 134 | @property 135 | def type(self) -> str: 136 | """Return the name of the remote.""" 137 | return self._type.upper() 138 | 139 | @property 140 | def available(self) -> bool: 141 | """Return True if entity is available.""" 142 | if not self._device.is_online: 143 | return False 144 | if self._type == TYPE_IR: 145 | return self._device.is_support_ir 146 | elif self._type == TYPE_RF: 147 | return self._device.is_support_rf 148 | else: 149 | return False 150 | 151 | @property 152 | def is_on(self) -> bool: 153 | """Return False if device is unreachable, else True.""" 154 | return self._device.is_online 155 | 156 | @property 157 | def hidden(self) -> bool: 158 | """Return if we should hide entity.""" 159 | return self._is_hidden 160 | 161 | @asyncio.coroutine 162 | def async_turn_on(self, **kwargs) -> None: 163 | """Turn the device on.""" 164 | _LOGGER.error("Device does not support turn_on, " 165 | "please use 'remote.send_command' to send commands.") 166 | 167 | @asyncio.coroutine 168 | def async_turn_off(self, **kwargs) -> None: 169 | """Turn the device off.""" 170 | _LOGGER.error("Device does not support turn_off, " 171 | "please use 'remote.send_command' to send commands.") 172 | 173 | async def async_update(self): 174 | from pykonkeio.error import DeviceOffline 175 | prev_available = self.available 176 | try: 177 | await self._device.update() 178 | except DeviceOffline: 179 | if prev_available: 180 | _LOGGER.warning('Device is offline %s', self.unique_id) 181 | 182 | async def _do_send_command(self, command): 183 | """Send a command.""" 184 | try: 185 | command_type, slot = command.split('_') 186 | except ValueError: 187 | _LOGGER.warning("Illegal command format: %s", command) 188 | return False 189 | 190 | if self._type != command_type: 191 | _LOGGER.warning("Illegal command type: %s", command) 192 | return False 193 | if self._type == TYPE_IR: 194 | return await self._device.ir_emit(slot) 195 | elif self._type == TYPE_RF: 196 | return await self._device.rf_emit(slot) 197 | 198 | async def async_send_command(self, command, **kwargs) -> None: 199 | """Send a command.""" 200 | num_repeats = kwargs.get(ATTR_NUM_REPEATS) 201 | delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) 202 | 203 | for _ in range(num_repeats): 204 | for item in command: 205 | await self._do_send_command(item) 206 | await asyncio.sleep(delay) 207 | 208 | async def async_learn(self, command, timeout=DEFAULT_TIMEOUT) -> bool: 209 | """Learn a command.""" 210 | if self._type == TYPE_IR: 211 | return await self._device.ir_learn(command, timeout=timeout) 212 | elif self._type == TYPE_RF: 213 | return await self._device.rf_learn(command, timeout=timeout) 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /custom_components/konke/switch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for the Konke outlet. 3 | 4 | For more details about this platform, please refer to the documentation at 5 | https://home-assistant.io/components/light.opple/ 6 | """ 7 | 8 | import logging 9 | import time 10 | 11 | import voluptuous as vol 12 | 13 | from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity 14 | from homeassistant.const import CONF_NAME, CONF_HOST 15 | import homeassistant.helpers.config_validation as cv 16 | 17 | REQUIREMENTS = ['pykonkeio>=2.1.7'] 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | DEFAULT_NAME = 'Konke Outlet' 22 | 23 | CONF_MODEL = 'model' 24 | MODEL_K1 = ['smart plugin', 'k1'] 25 | MODEL_K2 = ['k2', 'k2 pro'] 26 | MODEL_MINIK = ['minik', 'minik pro'] 27 | MODEL_MUL = ['mul'] 28 | MODEL_MICMUL = ['micmul'] 29 | 30 | MODEL_SWITCH = MODEL_K1 + MODEL_K2 + MODEL_MINIK 31 | MODEL_POWER_STRIP = MODEL_MUL + MODEL_MICMUL 32 | 33 | UPDATE_DEBONCE = 0.3 34 | 35 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 36 | vol.Required(CONF_HOST): cv.string, 37 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 38 | vol.Optional(CONF_MODEL): vol.In(MODEL_SWITCH + MODEL_POWER_STRIP) 39 | }) 40 | 41 | 42 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 43 | from pykonkeio.manager import get_device 44 | from pykonkeio.error import DeviceNotSupport 45 | name = config[CONF_NAME] 46 | host = config[CONF_HOST] 47 | model = config[CONF_MODEL].lower() 48 | entities = [] 49 | 50 | try: 51 | device = get_device(host, model) 52 | await device.update() 53 | except DeviceNotSupport: 54 | _LOGGER.error( 55 | 'Unsupported device found! Please create an issue at ' 56 | 'https://github.com/jedmeng/python-konkeio/issues ' 57 | 'and provide the following data: %s', model) 58 | return False 59 | 60 | if hasattr(device, 'socket_count'): 61 | powerstrip = KonkePowerStrip(device, name) 62 | for i in range(device.socket_count): 63 | entities.append(KonkePowerStripOutlet(powerstrip, name, i)) 64 | 65 | for i in range(device.usb_count or 0): 66 | entities.append(KonkePowerStripUSB(powerstrip, name, i)) 67 | else: 68 | entities.append(KonkeOutlet(name, device, model)) 69 | if hasattr(device, 'usb_status'): 70 | entities.append(KonkeUsbSwitch(name, device)) 71 | 72 | async_add_entities(entities) 73 | 74 | 75 | class KonkeOutlet(SwitchEntity): 76 | 77 | def __init__(self, name, device, model=None): 78 | self._name = name 79 | self._device = device 80 | self._model = model 81 | self._current_power_w = None 82 | 83 | @property 84 | def should_poll(self) -> bool: 85 | """Poll the plug.""" 86 | return True 87 | 88 | @property 89 | def available(self) -> bool: 90 | """Return True if outlet is available.""" 91 | return self._device.is_online 92 | 93 | @property 94 | def unique_id(self): 95 | """Return unique ID for light.""" 96 | return self._device.mac 97 | 98 | @property 99 | def name(self): 100 | """Return the display name of this outlet.""" 101 | return self._name 102 | 103 | @property 104 | def is_on(self): 105 | """Instruct the outlet to turn on.""" 106 | return self._device.status == 'open' 107 | 108 | @property 109 | def current_power_w(self): 110 | """Return the current power usage in W.""" 111 | return self._current_power_w 112 | 113 | async def async_turn_on(self, **kwargs): 114 | """Instruct the outlet to turn on.""" 115 | await self._device.turn_on() 116 | _LOGGER.debug("Turn on outlet %s", self.unique_id) 117 | 118 | async def async_turn_off(self, **kwargs): 119 | """Instruct the outlet to turn off.""" 120 | await self._device.turn_off() 121 | _LOGGER.debug("Turn off outlet %s", self.unique_id) 122 | 123 | async def async_update(self): 124 | """Synchronize state with outlet.""" 125 | from pykonkeio.error import DeviceOffline 126 | prev_available = self.available 127 | try: 128 | await self._device.update(type='relay') 129 | 130 | if self._model in MODEL_K2: 131 | self._current_power_w = await self._device.get_power() 132 | except DeviceOffline: 133 | if prev_available: 134 | _LOGGER.warning('Device is offline %s', self.entity_id) 135 | 136 | 137 | class KonkeUsbSwitch(SwitchEntity): 138 | def __init__(self, name, device): 139 | self._name = name 140 | self._device = device 141 | 142 | @property 143 | def should_poll(self) -> bool: 144 | """Poll the plug.""" 145 | return True 146 | 147 | @property 148 | def available(self) -> bool: 149 | """Return True if outlet is available.""" 150 | return self._device.is_online 151 | 152 | @property 153 | def unique_id(self): 154 | """Return unique ID for light.""" 155 | return '%s:usb' % self._device.mac 156 | 157 | @property 158 | def name(self): 159 | """Return the display name of this outlet.""" 160 | return '%s_usb' % self._name 161 | 162 | @property 163 | def is_on(self): 164 | """Instruct the outlet to turn on.""" 165 | return self._device.usb_status == 'open' 166 | 167 | async def async_turn_on(self, **kwargs): 168 | """Instruct the outlet to turn on.""" 169 | await self._device.turn_on_usb() 170 | _LOGGER.debug("Turn on usb %s", self.unique_id) 171 | 172 | async def async_turn_off(self, **kwargs): 173 | """Instruct the outlet to turn off.""" 174 | await self._device.turn_off_usb() 175 | _LOGGER.debug("Turn off usb %s", self.unique_id) 176 | 177 | async def async_update(self): 178 | """Synchronize state with outlet.""" 179 | from pykonkeio.error import DeviceOffline 180 | prev_available = self.available 181 | try: 182 | await self._device.update(type='usb') 183 | except DeviceOffline: 184 | if prev_available: 185 | _LOGGER.warning('Device is offline %s', self.entity_id) 186 | 187 | 188 | 189 | class KonkePowerStrip(object): 190 | 191 | def __init__(self, device, name: str): 192 | """Initialize the power strip.""" 193 | self._name = name 194 | self._device = device 195 | self._last_update = 0 196 | 197 | @property 198 | def available(self) -> bool: 199 | """Return True if outlet is available.""" 200 | return self._device.is_online 201 | 202 | @property 203 | def unique_id(self): 204 | """Return unique ID for outlet.""" 205 | return self._device.mac 206 | 207 | @property 208 | def name(self): 209 | """Return the display name of this outlet.""" 210 | return self._name 211 | 212 | def get_status(self, index): 213 | """Return true if outlet is on.""" 214 | return self._device.status[index] == 'open' 215 | 216 | def get_usb_status(self, index): 217 | """Return true if usb is on.""" 218 | return self._device.usb_status[index] == 'open' 219 | 220 | async def async_turn_on(self, index): 221 | """Instruct the outlet to turn on.""" 222 | await self._device.turn_on(index) 223 | 224 | async def async_turn_off(self, index): 225 | """Instruct the outlet to turn off.""" 226 | await self._device.turn_off(index) 227 | 228 | async def async_turn_on_usb(self, index): 229 | """Instruct the usb to turn on.""" 230 | await self._device.turn_on_usb(index) 231 | 232 | async def async_turn_off_usb(self, index): 233 | """Instruct the outlet to turn off.""" 234 | await self._device.turn_off_usb(index) 235 | 236 | async def async_update(self): 237 | """Synchronize state with power strip.""" 238 | from pykonkeio.error import DeviceOffline 239 | prev_available = self.available 240 | if time.time() - self._last_update >= UPDATE_DEBONCE: 241 | self._last_update = time.time() 242 | try: 243 | await self._device.update() 244 | except DeviceOffline: 245 | if prev_available: 246 | _LOGGER.warning('Device is offline %s', self.unique_id) 247 | 248 | 249 | class KonkePowerStripOutlet(SwitchEntity): 250 | """Outlet in Konke Power Strip.""" 251 | 252 | def __init__(self, powerstrip: KonkePowerStrip, name: str, index: int): 253 | """Initialize the outlet.""" 254 | self._powerstrip = powerstrip 255 | self._index = index 256 | self._name = name 257 | self._is_on = False 258 | 259 | @property 260 | def should_poll(self) -> bool: 261 | """Poll the plug.""" 262 | return True 263 | 264 | @property 265 | def available(self) -> bool: 266 | """Return True if outlet is available.""" 267 | return self._powerstrip.available 268 | 269 | @property 270 | def unique_id(self): 271 | """Return unique ID for outlet.""" 272 | return "%s:%s" % (self._powerstrip.unique_id, self._index + 1) 273 | 274 | @property 275 | def name(self): 276 | """Return the display name of this outlet.""" 277 | return '%s_%s' % (self._name, self._index + 1) 278 | 279 | @property 280 | def is_on(self): 281 | """Return true if outlet is on.""" 282 | return self._powerstrip.get_status(self._index) 283 | 284 | async def async_turn_on(self, **kwargs): 285 | """Instruct the outlet to turn on.""" 286 | await self._powerstrip.async_turn_on(self._index) 287 | _LOGGER.debug("Turn on outlet %s", self.unique_id) 288 | 289 | async def async_turn_off(self, **kwargs): 290 | """Instruct the outlet to turn off.""" 291 | await self._powerstrip.async_turn_off(self._index) 292 | _LOGGER.debug("Turn off outlet %s", self.unique_id) 293 | 294 | async def async_update(self): 295 | """Synchronize state with power strip.""" 296 | await self._powerstrip.async_update() 297 | 298 | 299 | class KonkePowerStripUSB(SwitchEntity): 300 | """Outlet in Konke Power Strip.""" 301 | 302 | def __init__(self, powerstrip: KonkePowerStrip, name: str, index: int): 303 | """Initialize the outlet.""" 304 | self._powerstrip = powerstrip 305 | self._index = index 306 | self._name = name 307 | 308 | @property 309 | def should_poll(self) -> bool: 310 | """Poll the plug.""" 311 | return True 312 | 313 | @property 314 | def available(self) -> bool: 315 | """Return True if outlet is available.""" 316 | return self._powerstrip.available 317 | 318 | @property 319 | def unique_id(self): 320 | """Return unique ID for outlet.""" 321 | return "%s:usb_%s" % (self._powerstrip.unique_id, self._index + 1) 322 | 323 | @property 324 | def name(self): 325 | """Return the display name of this outlet.""" 326 | return '%s_usb%s' % (self._name, self._index + 1) 327 | 328 | @property 329 | def is_on(self): 330 | """Return true if outlet is on.""" 331 | return self._powerstrip.get_usb_status(self._index) 332 | 333 | async def async_turn_on(self, **kwargs): 334 | """Instruct the outlet to turn on.""" 335 | await self._powerstrip.async_turn_on_usb(self._index) 336 | _LOGGER.debug("Turn on outlet %s", self.unique_id) 337 | 338 | async def async_turn_off(self, **kwargs): 339 | """Instruct the outlet to turn off.""" 340 | await self._powerstrip.async_turn_off_usb(self._index) 341 | _LOGGER.debug("Turn off outlet %s", self.unique_id) 342 | 343 | async def async_update(self): 344 | """Synchronize state with power strip.""" 345 | await self._powerstrip.async_update() 346 | --------------------------------------------------------------------------------