├── .gitignore ├── README.md ├── floureon ├── README.md ├── __init__.py ├── climate.py ├── manifest.json └── switch.py └── secolink ├── alarm_control_panel.py └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | custom_updater.py 2 | smartir/ 3 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repository is archived. 2 | 3 | # For updated components, please check: 4 | 5 | - [Floureon Thermostat](https://github.com/algirdasc/hass-floureon) 6 | - [Secolink Alarm Panel](https://github.com/algirdasc/hass-secolink) 7 | -------------------------------------------------------------------------------- /floureon/README.md: -------------------------------------------------------------------------------- 1 | # Your support 2 | Buy Me A Coffee 3 | 4 | # Intro 5 | Component for controlling Floureon or other chinese-based WiFi smart thermostat (Beok and others). Climate component will have 3 modes: "auto" (in which will used thermostat's internal schedule), "heat (which is "manual" mode) and "off". Also, while in "heat" mode it is possible to use preset "away". Changing mode to other than "heat" will set preset to "none". 6 | 7 | If you want to use custom or more advanced controll, you should use switch component and generic thermostat in Home Assistant instead. See below for configuration. 8 | 9 | # Configuration as a Climate 10 | 11 | | Name | Type | Default | Description | 12 | |------|:----:|:-------:|-------------| 13 | | host ***(required)*** | string | | IP or hostname of thermostat 14 | | mac ***(required)*** | string | | MAC address of thermostat, ex. `AB:CD:EF:00:11:22` 15 | | name ***(required)*** | string | | Set a custom name which is displayed beside the icon. 16 | | schedule | integer | `0` | Set which schedule to use (0 - `12345,67`, 1 - `123456,7`, 2 - `1234567`) 17 | | use_external_temp | boolen | `true` | Set to false if you want to use thermostat`s internal temperature sensor for temperature calculation 18 | 19 | #### Example: 20 | ```yaml 21 | climate: 22 | platform: floureon 23 | name: livingroom_floor 24 | mac: 78:0f:77:00:00:00 25 | host: 192.168.0.1 26 | use_external_temp: false 27 | ``` 28 | 29 | # Configuration as a Switch 30 | | Name | Type | Default | Description | 31 | |------|:----:|:-------:|-------------| 32 | | host ***(required)*** | string | | IP or hostname of thermostat 33 | | mac ***(required)*** | string | | MAC address of thermostat, ex. `AB:CD:EF:00:11:22` 34 | | name ***(required)*** | string | | Set a custom name which is displayed beside the icon. 35 | | turn_off_mode | string | `min_temp` | Thermostat turn off. Set to `min_temp` and thermostat will be turned off by setting minimum temperature, `turn_off` - thermostat will be turned off by turning it off completely. 36 | | turn_on_mode | string, float | `max_temp` | Thermostat turn on mode. Set to `max_temp` - thermostat will be turned on by setting maximum temperature, `float` - thermostat will be turned on by set temperature, ex. `20.5`. ***Note, that `.5` or `.0` is mandatory *** 37 | | use_external_temp | boolen | `true` | Set to false if you want to use thermostat`s internal temperature sensor for temperature calculation 38 | #### Example: 39 | ```yaml 40 | switch: 41 | platform: floureon 42 | name: livingroom_floor 43 | mac: 78:0f:77:00:00:00 44 | host: 192.168.0.1 45 | turn_off_mode: min_temp 46 | turn_on_mode: 23.5 47 | ``` 48 | -------------------------------------------------------------------------------- /floureon/__init__.py: -------------------------------------------------------------------------------- 1 | import broadlink 2 | import logging 3 | from datetime import datetime 4 | from socket import timeout 5 | 6 | _LOGGER = logging.getLogger(__name__) 7 | 8 | BROADLINK_ACTIVE = 1 9 | BROADLINK_IDLE = 0 10 | BROADLINK_POWER_ON = 1 11 | BROADLINK_POWER_OFF = 0 12 | BROADLINK_MODE_AUTO = 1 # or 2? 13 | BROADLINK_MODE_MANUAL = 0 14 | BROADLINK_SENSOR_INTERNAL = 0 15 | BROADLINK_SENSOR_EXTERNAL = 1 16 | BROADLINK_SENSOR_BOTH = 2 17 | BROADLINK_TEMP_AUTO = 0 18 | BROADLINK_TEMP_MANUAL = 1 19 | 20 | CONF_HOST = 'host' 21 | CONF_MAC = 'mac' 22 | CONF_USE_EXTERNAL_TEMP = 'use_external_temp' 23 | CONF_SCHEDULE = 'schedule' 24 | 25 | DEFAULT_SCHEDULE = 0 26 | DEFAULT_USE_EXTERNAL_TEMP = True 27 | 28 | class BroadlinkThermostat: 29 | 30 | def __init__(self, host, mac): 31 | self._host = host 32 | self._port = 80 33 | self._mac = bytes.fromhex(''.join(reversed(mac.split(':')))) 34 | 35 | def device(self): 36 | return broadlink.gendevice(0x4EAD, (self._host, self._port), self._mac) 37 | 38 | def thermostat_set_time(self): 39 | """Set thermostat time""" 40 | try: 41 | device = self.device() 42 | if device.auth(): 43 | now = datetime.now() 44 | device.set_time(now.hour, 45 | now.minute, 46 | now.second, 47 | now.weekday() + 1) 48 | except timeout: 49 | pass 50 | except Exception as e: 51 | _LOGGER.error("Thermostat %s set_time error: %s", self._host, str(e)) 52 | 53 | def thermostat_read_status(self): 54 | """Read thermostat data""" 55 | data = None 56 | try: 57 | device = self.device() 58 | if device.auth(): 59 | data = device.get_full_status() 60 | except timeout: 61 | pass 62 | except Exception as e: 63 | _LOGGER.warning("Thermostat %s read_status error: %s", self._host, str(e)) 64 | finally: 65 | return data 66 | -------------------------------------------------------------------------------- /floureon/climate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from socket import timeout 3 | from typing import List, Optional 4 | 5 | import voluptuous as vol 6 | 7 | from custom_components.floureon import ( 8 | BroadlinkThermostat, 9 | CONF_HOST, 10 | CONF_MAC, 11 | CONF_USE_EXTERNAL_TEMP, 12 | CONF_SCHEDULE, 13 | DEFAULT_SCHEDULE, 14 | DEFAULT_USE_EXTERNAL_TEMP, 15 | BROADLINK_ACTIVE, 16 | BROADLINK_IDLE, 17 | BROADLINK_POWER_ON, 18 | BROADLINK_POWER_OFF, 19 | BROADLINK_MODE_AUTO, 20 | BROADLINK_MODE_MANUAL, 21 | BROADLINK_SENSOR_INTERNAL, 22 | BROADLINK_SENSOR_EXTERNAL, 23 | BROADLINK_TEMP_AUTO, 24 | BROADLINK_TEMP_MANUAL 25 | ) 26 | 27 | from homeassistant.components.climate import ClimateEntity, PLATFORM_SCHEMA 28 | from homeassistant.helpers.restore_state import RestoreEntity 29 | from homeassistant.util.temperature import convert as convert_temperature 30 | from homeassistant.components.climate.const import ( 31 | HVAC_MODE_OFF, 32 | HVAC_MODE_HEAT, 33 | HVAC_MODE_AUTO, 34 | CURRENT_HVAC_OFF, 35 | CURRENT_HVAC_HEAT, 36 | CURRENT_HVAC_IDLE, 37 | PRESET_NONE, 38 | PRESET_AWAY, 39 | SUPPORT_TARGET_TEMPERATURE, 40 | SUPPORT_PRESET_MODE, 41 | DEFAULT_MIN_TEMP, 42 | DEFAULT_MAX_TEMP 43 | ) 44 | 45 | from homeassistant.const import ( 46 | PRECISION_HALVES, 47 | ATTR_TEMPERATURE, 48 | PRECISION_HALVES, 49 | TEMP_CELSIUS, 50 | CONF_NAME 51 | ) 52 | 53 | import homeassistant.helpers.config_validation as cv 54 | 55 | _LOGGER = logging.getLogger(__name__) 56 | 57 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 58 | vol.Required(CONF_HOST): cv.string, 59 | vol.Required(CONF_MAC): cv.string, 60 | vol.Required(CONF_NAME): cv.string, 61 | vol.Optional(CONF_SCHEDULE, default=DEFAULT_SCHEDULE): vol.All(int, vol.Range(min=0,max=2)), 62 | vol.Optional(CONF_USE_EXTERNAL_TEMP, default=DEFAULT_USE_EXTERNAL_TEMP): cv.boolean, 63 | }) 64 | 65 | 66 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 67 | """Set up the generic thermostat platform.""" 68 | async_add_entities([FloureonClimate(config)]) 69 | 70 | 71 | class FloureonClimate(ClimateEntity, RestoreEntity): 72 | 73 | def __init__(self, config): 74 | self._thermostat = BroadlinkThermostat(config.get(CONF_HOST), config.get(CONF_MAC)) 75 | 76 | self._name = config.get(CONF_NAME) 77 | self._use_external_temp = config.get(CONF_USE_EXTERNAL_TEMP) 78 | 79 | self._min_temp = DEFAULT_MIN_TEMP 80 | self._max_temp = DEFAULT_MAX_TEMP 81 | self._room_temp = None 82 | self._external_temp = None 83 | 84 | self._away_setpoint = DEFAULT_MIN_TEMP 85 | self._manual_setpoint = DEFAULT_MIN_TEMP 86 | 87 | self._preset_mode = None 88 | 89 | self._thermostat_loop_mode = config.get(CONF_SCHEDULE) 90 | self._thermostat_current_action = None 91 | self._thermostat_current_mode = None 92 | self._thermostat_current_temp = None 93 | self._thermostat_target_temp = None 94 | 95 | def thermostat_get_sensor(self) -> int: 96 | """Get sensor to use""" 97 | return BROADLINK_SENSOR_EXTERNAL if self._use_external_temp is True else BROADLINK_SENSOR_INTERNAL 98 | 99 | @property 100 | def name(self) -> str: 101 | """Return thermostat name""" 102 | return self._name 103 | 104 | @property 105 | def precision(self) -> float: 106 | """Return the precision of the system.""" 107 | return PRECISION_HALVES 108 | 109 | @property 110 | def temperature_unit(self) -> str: 111 | """Return the unit of measurement.""" 112 | return TEMP_CELSIUS 113 | 114 | @property 115 | def hvac_mode(self) -> str: 116 | """Return hvac operation ie. heat, cool mode. 117 | Need to be one of HVAC_MODE_*. 118 | """ 119 | return self._thermostat_current_mode 120 | 121 | @property 122 | def hvac_modes(self) -> List[str]: 123 | """Return the list of available hvac operation modes. 124 | Need to be a subset of HVAC_MODES. 125 | """ 126 | return [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] 127 | 128 | @property 129 | def hvac_action(self) -> Optional[str]: 130 | """Return the current running hvac operation if supported. 131 | Need to be one of CURRENT_HVAC_*. 132 | """ 133 | return self._thermostat_current_action 134 | 135 | @property 136 | def preset_mode(self) -> Optional[str]: 137 | """Return the current preset mode, e.g., home, away, temp. 138 | Requires SUPPORT_PRESET_MODE. 139 | """ 140 | return self._preset_mode 141 | 142 | @property 143 | def preset_modes(self) -> Optional[List[str]]: 144 | """Return a list of available preset modes. 145 | Requires SUPPORT_PRESET_MODE. 146 | """ 147 | return [PRESET_NONE, PRESET_AWAY] 148 | 149 | @property 150 | def current_temperature(self) -> Optional[float]: 151 | """Return the current temperature.""" 152 | return self._thermostat_current_temp 153 | 154 | @property 155 | def target_temperature(self) -> Optional[float]: 156 | """Return the temperature we try to reach.""" 157 | return self._thermostat_target_temp 158 | 159 | @property 160 | def supported_features(self): 161 | """Return the list of supported features.""" 162 | return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE 163 | 164 | @property 165 | def min_temp(self) -> float: 166 | """Return the minimum temperature.""" 167 | return convert_temperature(self._min_temp, TEMP_CELSIUS, 168 | self.temperature_unit) 169 | 170 | @property 171 | def max_temp(self) -> float: 172 | """Return the maximum temperature.""" 173 | return convert_temperature(self._max_temp, TEMP_CELSIUS, 174 | self.temperature_unit) 175 | 176 | @property 177 | def device_state_attributes(self) -> dict: 178 | """Return the attribute(s) of the sensor""" 179 | return { 180 | 'away_setpoint': self._away_setpoint, 181 | 'manual_setpoint': self._manual_setpoint, 182 | 'external_temp': self._external_temp, 183 | 'room_temp': self._room_temp, 184 | 'loop_mode': self._thermostat_loop_mode 185 | } 186 | 187 | async def async_added_to_hass(self) -> None: 188 | """Run when entity about to added.""" 189 | await super().async_added_to_hass() 190 | 191 | # Set thermostat time 192 | self._thermostat.thermostat_set_time() 193 | 194 | # Restore 195 | last_state = await self.async_get_last_state() 196 | 197 | if last_state is not None: 198 | for param in ['away_setpoint', 'manual_setpoint']: 199 | if param in last_state.attributes: 200 | setattr(self, '_{0}'.format(param), last_state.attributes[param]) 201 | 202 | async def async_set_temperature(self, **kwargs) -> None: 203 | """Set new target temperature.""" 204 | if kwargs.get(ATTR_TEMPERATURE) is not None: 205 | target_temp = float(kwargs.get(ATTR_TEMPERATURE)) 206 | try: 207 | device = self._thermostat.device() 208 | if device.auth(): 209 | # device.set_power(BROADLINK_POWER_ON) 210 | device.set_mode(BROADLINK_MODE_MANUAL, self._thermostat_loop_mode, self.thermostat_get_sensor()) 211 | device.set_temp(target_temp) 212 | 213 | # Save temperatures for future use 214 | if self._preset_mode == PRESET_AWAY: 215 | self._away_setpoint = target_temp 216 | elif self._preset_mode == PRESET_NONE: 217 | self._manual_setpoint = target_temp 218 | except timeout: 219 | pass 220 | 221 | await self.async_update_ha_state() 222 | 223 | async def async_set_hvac_mode(self, hvac_mode) -> None: 224 | """Set operation mode.""" 225 | try: 226 | device = self._thermostat.device() 227 | if device.auth(): 228 | if hvac_mode == HVAC_MODE_OFF: 229 | device.set_power(BROADLINK_POWER_OFF) 230 | else: 231 | device.set_power(BROADLINK_POWER_ON) 232 | if hvac_mode == HVAC_MODE_AUTO: 233 | device.set_mode(BROADLINK_MODE_AUTO, self._thermostat_loop_mode, self.thermostat_get_sensor()) 234 | elif hvac_mode == HVAC_MODE_HEAT: 235 | device.set_mode(BROADLINK_MODE_MANUAL, self._thermostat_loop_mode, self.thermostat_get_sensor()) 236 | except timeout: 237 | pass 238 | 239 | await self.async_update_ha_state() 240 | 241 | async def async_set_preset_mode(self, preset_mode) -> None: 242 | """Set new preset mode.""" 243 | self._preset_mode = preset_mode 244 | 245 | try: 246 | device = self._thermostat.device() 247 | if device.auth(): 248 | device.set_power(BROADLINK_POWER_ON) 249 | device.set_mode(BROADLINK_MODE_MANUAL, self._thermostat_loop_mode, self.thermostat_get_sensor()) 250 | if self._preset_mode == PRESET_AWAY: 251 | device.set_temp(self._away_setpoint) 252 | elif self._preset_mode == PRESET_NONE: 253 | device.set_temp(self._manual_setpoint) 254 | except timeout: 255 | pass 256 | 257 | await self.async_update_ha_state() 258 | 259 | async def async_turn_off(self) -> None: 260 | """Turn thermostat off""" 261 | await self.async_set_hvac_mode(HVAC_MODE_OFF) 262 | 263 | async def async_turn_on(self) -> None: 264 | """Turn thermostat on""" 265 | await self.async_set_hvac_mode(HVAC_MODE_AUTO) 266 | 267 | async def async_update(self) -> None: 268 | """Get thermostat info""" 269 | data = self._thermostat.thermostat_read_status() 270 | 271 | if not data: 272 | return 273 | 274 | # Temperatures 275 | self._room_temp = data['room_temp'] 276 | self._external_temp = data['external_temp'] 277 | 278 | self._thermostat_current_temp = data['external_temp'] if self._use_external_temp else data['room_temp'] 279 | 280 | # self._hysteresis = int(data['dif']) 281 | self._min_temp = int(data['svl']) 282 | self._max_temp = int(data['svh']) 283 | self._thermostat_target_temp = data['thermostat_temp'] 284 | 285 | # Thermostat modes & status 286 | if data["power"] == BROADLINK_POWER_OFF: 287 | # Unset away mode 288 | self._preset_mode = PRESET_NONE 289 | self._thermostat_current_mode = HVAC_MODE_OFF 290 | else: 291 | # Set mode to manual when overridden auto mode or thermostat is in manual mode 292 | if data["auto_mode"] == BROADLINK_MODE_MANUAL or data['temp_manual'] == BROADLINK_TEMP_MANUAL: 293 | self._thermostat_current_mode = HVAC_MODE_HEAT 294 | else: 295 | # Unset away mode 296 | self._preset_mode = PRESET_NONE 297 | self._thermostat_current_mode = HVAC_MODE_AUTO 298 | 299 | # Thermostat action 300 | if data["power"] == BROADLINK_POWER_ON and data["active"] == BROADLINK_ACTIVE: 301 | self._thermostat_current_action = CURRENT_HVAC_HEAT 302 | elif data["power"] == BROADLINK_POWER_ON and data["active"] == BROADLINK_IDLE: 303 | self._thermostat_current_action = CURRENT_HVAC_IDLE 304 | elif data["power"] == BROADLINK_POWER_OFF: 305 | self._thermostat_current_action = CURRENT_HVAC_OFF 306 | -------------------------------------------------------------------------------- /floureon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "floureon", 3 | "name": "Floureon Thermostat", 4 | "documentation": "https://github.com/algirdasc/hass-components", 5 | "dependencies": [], 6 | "codeowners": [], 7 | "requirements": ["pythoncrc", "broadlink==0.14.0"] 8 | } 9 | -------------------------------------------------------------------------------- /floureon/switch.py: -------------------------------------------------------------------------------- 1 | from socket import timeout 2 | from custom_components.floureon import ( 3 | BroadlinkThermostat, 4 | CONF_HOST, 5 | CONF_MAC, 6 | CONF_USE_EXTERNAL_TEMP, 7 | CONF_USE_EXTERNAL_TEMP, 8 | DEFAULT_SCHEDULE, 9 | DEFAULT_USE_EXTERNAL_TEMP, 10 | BROADLINK_POWER_ON, 11 | BROADLINK_POWER_OFF, 12 | BROADLINK_MODE_MANUAL, 13 | BROADLINK_ACTIVE, 14 | BROADLINK_SENSOR_EXTERNAL, 15 | BROADLINK_SENSOR_INTERNAL 16 | ) 17 | 18 | import logging 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | import voluptuous as vol 22 | 23 | from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA 24 | from homeassistant.helpers.restore_state import RestoreEntity 25 | from homeassistant.const import ( 26 | CONF_NAME, 27 | CONF_PLATFORM, 28 | STATE_UNAVAILABLE, 29 | STATE_ON, 30 | STATE_OFF 31 | ) 32 | from homeassistant.components.climate.const import ( 33 | DEFAULT_MIN_TEMP, 34 | DEFAULT_MAX_TEMP 35 | ) 36 | 37 | import homeassistant.helpers.config_validation as cv 38 | 39 | BROADLINK_TURN_OFF = 'turn_off' 40 | BROADLINK_MIN_TEMP = 'min_temp' 41 | BROADLINK_MAX_TEMP = 'max_temp' 42 | 43 | DEFAULT_TURN_OFF_MODE = BROADLINK_MIN_TEMP 44 | DEFAULT_TURN_ON_MODE = BROADLINK_MAX_TEMP 45 | 46 | CONF_TURN_OFF_MODE = 'turn_off_mode' 47 | CONF_TURN_ON_MODE = 'turn_on_mode' 48 | 49 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 50 | vol.Required(CONF_HOST): cv.string, 51 | vol.Required(CONF_MAC): cv.string, 52 | vol.Required(CONF_NAME): cv.string, 53 | vol.Optional(CONF_USE_EXTERNAL_TEMP, default=DEFAULT_USE_EXTERNAL_TEMP): cv.boolean, 54 | vol.Optional(CONF_TURN_OFF_MODE, default=DEFAULT_TURN_OFF_MODE): vol.Any(BROADLINK_MIN_TEMP, BROADLINK_TURN_OFF), 55 | vol.Optional(CONF_TURN_ON_MODE, default=DEFAULT_TURN_ON_MODE): vol.Any(float, BROADLINK_MAX_TEMP) 56 | }) 57 | 58 | 59 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 60 | """Set up the platform.""" 61 | async_add_entities([FloureonSwitch(config)]) 62 | 63 | 64 | class FloureonSwitch(SwitchDevice, RestoreEntity): 65 | 66 | def __init__(self, config): 67 | self._thermostat = BroadlinkThermostat(config.get(CONF_HOST), config.get(CONF_MAC)) 68 | 69 | self._name = config.get(CONF_NAME) 70 | 71 | self._min_temp = DEFAULT_MIN_TEMP 72 | self._max_temp = DEFAULT_MAX_TEMP 73 | self._thermostat_current_temp = None 74 | 75 | self._turn_on_mode = config.get(CONF_TURN_ON_MODE) 76 | self._turn_off_mode = config.get(CONF_TURN_OFF_MODE) 77 | self._use_external_temp = config.get(CONF_USE_EXTERNAL_TEMP) 78 | 79 | self._state = STATE_UNAVAILABLE 80 | 81 | def thermostat_get_sensor(self) -> int: 82 | """Get sensor to use""" 83 | return BROADLINK_SENSOR_EXTERNAL if self._use_external_temp is True else BROADLINK_SENSOR_INTERNAL 84 | 85 | @property 86 | def name(self) -> str: 87 | """Return the name of the device if any.""" 88 | return self._name 89 | 90 | @property 91 | def is_on(self) -> bool: 92 | """Return thermostat state on / off""" 93 | return self._state == STATE_ON 94 | 95 | async def async_turn_on(self, **kwargs) -> None: 96 | """Turn the entity on""" 97 | try: 98 | device = self._thermostat.device() 99 | if device.auth(): 100 | device.set_power(BROADLINK_POWER_ON) 101 | device.set_mode(BROADLINK_MODE_MANUAL, 0, self.thermostat_get_sensor()) 102 | device.set_temp(self._max_temp if self._turn_on_mode == BROADLINK_MAX_TEMP else self._turn_on_mode) 103 | except timeout: 104 | pass 105 | 106 | self._state = STATE_ON 107 | await self.async_update_ha_state() 108 | 109 | async def async_turn_off(self, **kwargs) -> None: 110 | """Turn the entity off""" 111 | try: 112 | device = self._thermostat.device() 113 | if device.auth(): 114 | if self._turn_off_mode == BROADLINK_TURN_OFF: 115 | device.set_power(BROADLINK_POWER_OFF) 116 | else: 117 | device.set_mode(BROADLINK_MODE_MANUAL, 0, self.thermostat_get_sensor()) 118 | device.set_temp(self._min_temp) 119 | except timeout: 120 | pass 121 | 122 | self._state = STATE_OFF 123 | await self.async_update_ha_state() 124 | 125 | async def async_update(self) -> None: 126 | """Get thermostat info""" 127 | data = self._thermostat.thermostat_read_status() 128 | if not data: 129 | self._state = STATE_UNAVAILABLE 130 | return 131 | 132 | self._min_temp = int(data['svl']) 133 | self._max_temp = int(data['svh']) 134 | self._state = STATE_ON if data['power'] == BROADLINK_POWER_ON and data['active'] == BROADLINK_ACTIVE else STATE_OFF 135 | self._thermostat_current_temp = data['external_temp'] if self._use_external_temp else data['room_temp'] 136 | -------------------------------------------------------------------------------- /secolink/alarm_control_panel.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import sys 4 | import socketserver 5 | import threading 6 | import asyncio 7 | import datetime 8 | 9 | import homeassistant.components.alarm_control_panel as alarm 10 | from homeassistant.components.alarm_control_panel.const import ( 11 | SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, SUPPORT_ALARM_ARM_NIGHT 12 | ) 13 | from homeassistant.const import ( 14 | STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, 15 | STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | QUAL_OPEN = 1 20 | QUAL_CLOSE = 3 21 | 22 | 23 | def setup_platform(hass, config, add_devices, discovery_info=None): 24 | add_devices([SecolinkAlarm( 25 | hass, config 26 | )]) 27 | 28 | 29 | class SecolinkAlarm(alarm.AlarmControlPanel): 30 | 31 | def __init__(self, hass, config): 32 | self._name = str(config.get('name')) 33 | self._username = str(config.get('username', '')) 34 | self._password = str(config.get('password', '')) 35 | self._clientid = str(config.get('clientid', '0000')) 36 | self._listen_ip = str(config.get('listen_ip', '0.0.0.0')) 37 | self._listen_port = int(config.get('listen_port', 8125)) 38 | 39 | self._last_heartbeat = None 40 | self._last_event_at = None 41 | self._last_event_type = None 42 | self._last_event_zone = None 43 | self._last_event_area = None 44 | self._last_event_qual = None 45 | self._changed_by = None 46 | self._state = STATE_UNKNOWN 47 | 48 | server = ThreadedTCPServer((self._listen_ip, self._listen_port), ThreadedTCPRequestHandler) 49 | server.secolink = self 50 | 51 | # Start a thread with the server -- that thread will then start one 52 | # more thread for each request 53 | server_thread = threading.Thread(target=server.serve_forever) 54 | 55 | # Exit the server thread when the main thread terminates 56 | server_thread.daemon = True 57 | server_thread.start() 58 | 59 | @property 60 | def should_poll(self): 61 | return False 62 | 63 | @property 64 | def name(self): 65 | return self._name 66 | 67 | @property 68 | def state(self): 69 | return self._state 70 | 71 | @property 72 | def code_format(self): 73 | return '^\d+{4,6}' 74 | 75 | @property 76 | def changed_by(self): 77 | return self._changed_by 78 | 79 | @property 80 | def supported_features(self): 81 | return SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT 82 | 83 | @property 84 | def device_state_attributes(self): 85 | """Return the state attributes.""" 86 | state_attr = {} 87 | 88 | state_attr['last_heartbeat'] = self._last_heartbeat 89 | state_attr['last_event_type'] = self._last_event_type 90 | state_attr['last_event_zone'] = self._last_event_zone 91 | state_attr['last_event_area'] = self._last_event_area 92 | state_attr['last_event_qual'] = self._last_event_qual 93 | state_attr['last_event_at'] = self._last_event_at 94 | 95 | return state_attr 96 | 97 | @asyncio.coroutine 98 | def async_alarm_disarm(self, code=None): 99 | """Send disarm command.""" 100 | _LOGGER.debug("alarm_disarm: %s", code) 101 | if code: 102 | _LOGGER.debug("alarm_disarm: sending %s1", str(code)) 103 | pass 104 | 105 | @asyncio.coroutine 106 | def async_alarm_arm_away(self, code=None): 107 | """Send arm away command.""" 108 | _LOGGER.debug("alarm_arm_away: %s", code) 109 | if code: 110 | _LOGGER.debug("alarm_arm_away: sending %s2", str(code)) 111 | pass 112 | 113 | @asyncio.coroutine 114 | def async_alarm_arm_home(self, code=None): 115 | """Send arm home command.""" 116 | _LOGGER.debug("alarm_arm_home: %s", code) 117 | if code: 118 | _LOGGER.debug("alarm_arm_home: sending %s3", str(code)) 119 | pass 120 | 121 | 122 | class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): 123 | 124 | def handle(self): 125 | try: 126 | data = self.request.recv(32).strip() 127 | if not data: 128 | return 129 | 130 | data = data.decode('utf-8') 131 | match = re.match('^(.*?),(.*?),(\d{4}),18(\d)(\d{3})([A-F0-9]{2})(\d{3})$', data) 132 | if not match: 133 | _LOGGER.warning("Received unknown message from {0}: {1}".format(self.client_address[0], data)) 134 | return 135 | 136 | event_username = match.group(1) 137 | event_password = match.group(2) 138 | event_clientid = match.group(3) 139 | 140 | if not event_username == self.server.secolink._username: 141 | _LOGGER.warning("Wrong username '{0}' from {1}".format(event_username, self.client_address[0])) 142 | return 143 | 144 | if not event_password == self.server.secolink._password: 145 | _LOGGER.warning("Wrong password '{0}' from {1}".format(event_password, self.client_address[0])) 146 | return 147 | 148 | if not event_clientid == self.server.secolink._clientid: 149 | _LOGGER.warning("Wrong Client ID '{0}' from {1}".format(event_clientid, self.client_address[0])) 150 | return 151 | 152 | self.request.send('ACK'.encode('utf-8')) 153 | 154 | event_qual = match.group(4) 155 | event_type = match.group(5) 156 | event_area = match.group(6) 157 | event_zone = match.group(7) 158 | 159 | _LOGGER.debug("Received event from {0}. Type: {1}, Area {2}, Zone {3}, Qualifier {4}".format( 160 | self.client_address[0], event_type, event_area, event_zone, event_qual)) 161 | 162 | event_type = int(event_type) 163 | event_qual = int(event_qual) 164 | 165 | is_heartbeat = False 166 | if 100 <= event_type < 200: # ALARMS 167 | self.server.secolink._state = STATE_ALARM_TRIGGERED 168 | self.server.secolink._changed_by = event_zone 169 | elif 400 <= event_type < 410: # ARM / DISARM 170 | if event_qual == QUAL_OPEN: 171 | self.server.secolink._state = STATE_ALARM_DISARMED 172 | self.server.secolink._changed_by = event_zone 173 | elif event_qual == QUAL_CLOSE: 174 | self.server.secolink._state = STATE_ALARM_ARMED_AWAY 175 | self.server.secolink._changed_by = event_zone 176 | elif event_type == 441 and re.match(r'1\d\d', event_zone): # STAY 177 | if event_qual == QUAL_OPEN: 178 | self.server.secolink._state = STATE_ALARM_DISARMED 179 | self.server.secolink._changed_by = event_zone 180 | elif event_qual == QUAL_CLOSE: 181 | self.server.secolink._state = STATE_ALARM_ARMED_HOME 182 | self.server.secolink._changed_by = event_zone 183 | elif event_type == 441 and re.match(r'2\d\d', event_zone): # NIGHT 184 | if event_qual == QUAL_OPEN: 185 | self.server.secolink._state = STATE_ALARM_DISARMED 186 | self.server.secolink._changed_by = event_zone 187 | elif event_qual == QUAL_CLOSE: 188 | self.server.secolink._state = STATE_ALARM_ARMED_NIGHT 189 | self.server.secolink._changed_by = event_zone 190 | elif event_type == 602: # HEARTBEAT 191 | self.server.secolink._last_heartbeat = datetime.datetime.now() 192 | is_heartbeat = True 193 | 194 | if is_heartbeat is not True: 195 | self.server.secolink._last_event_at = datetime.datetime.now() 196 | self.server.secolink._last_event_type = event_type 197 | self.server.secolink._last_event_area = event_area 198 | self.server.secolink._last_event_zone = event_zone 199 | self.server.secolink._last_event_qual = event_qual 200 | 201 | self.server.secolink.async_schedule_update_ha_state() 202 | 203 | except Exception as ex: 204 | exc_type, exc_obj, exc_tb = sys.exc_info() 205 | _LOGGER.error("Error parsing CSV IP message from {0}".format(self.client_address[0])) 206 | _LOGGER.error("Error: {0}".format(str(ex))) 207 | _LOGGER.error("Line: {0}".format(exc_tb.tb_lineno)) 208 | 209 | 210 | class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 211 | daemon_threads = True 212 | allow_reuse_address = True 213 | -------------------------------------------------------------------------------- /secolink/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "secolink", 3 | "name": "Secolink", 4 | "documentation": "https://github.com/algirdasc/hass-components", 5 | "dependencies": [], 6 | "codeowners": [], 7 | "requirements": [] 8 | } 9 | --------------------------------------------------------------------------------