├── .gitignore ├── README.md ├── edp_redy ├── README.md ├── edp_redy.py ├── sensor │ └── edp_redy.py └── switch │ └── edp_redy.py ├── others ├── README.md ├── device_tracker_sensor │ ├── __init__.py │ └── binary_sensor.py ├── edp_redy_local │ ├── __init__.py │ └── sensor.py └── timed_state_infer │ ├── __init__.py │ └── binary_sensor.py └── whirlpool ├── README.md ├── __init__.py ├── climate.py └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homeassistant-custom-components 2 | My custom components for Home Assistant 3 | -------------------------------------------------------------------------------- /edp_redy/README.md: -------------------------------------------------------------------------------- 1 | # NOTE: this is currently not working in the lastest EDP re:dy BETA servers. 2 | 3 | ### edp_redy: 4 | Copy the following files to your custom_components folder (creating the requires subfolders): 5 | - edp_redy.py 6 | - sensor/edp_redy.py 7 | - switch/edp_redy.py 8 | 9 | Add the following configuration: 10 | 11 | ``` 12 | edp_redy: 13 | username: 'xxxxx' 14 | password: 'xxxxx' 15 | ``` 16 | -------------------------------------------------------------------------------- /edp_redy/edp_redy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for EDP re:dy. 3 | 4 | For more details about this component, please refer to the documentation at 5 | https://home-assistant.io/components/edp_redy/ 6 | """ 7 | 8 | import aiohttp 9 | import asyncio 10 | import json 11 | import logging 12 | 13 | import async_timeout 14 | from datetime import timedelta 15 | 16 | import voluptuous as vol 17 | 18 | from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, 19 | EVENT_HOMEASSISTANT_START) 20 | from homeassistant.core import callback 21 | from homeassistant.helpers import discovery, dispatcher, aiohttp_client 22 | import homeassistant.helpers.config_validation as cv 23 | from homeassistant.helpers.entity import Entity 24 | from homeassistant.helpers.event import async_track_point_in_time 25 | from homeassistant.util import dt as dt_util 26 | 27 | _LOGGER = logging.getLogger(__name__) 28 | 29 | DOMAIN = 'edp_redy' 30 | EDP_REDY = "edp_redy" 31 | DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) 32 | ACTIVE_POWER_ID = "home_active_power" 33 | 34 | URL_BASE = "https://redy.edp.pt/EdpPortal/" 35 | URL_LOGIN_PAGE = URL_BASE 36 | URL_GET_ACTIVE_POWER = "{0}/Consumption/GetActivePower".format(URL_BASE) 37 | URL_GET_SWITCH_MODULES = "{0}/HomeAutomation/GetSwitchModules".format(URL_BASE) 38 | URL_SET_STATE_VAR = "{0}/HomeAutomation/SetStateVar".format(URL_BASE) 39 | URL_LOGOUT = "{0}/Login/Logout".format(URL_BASE) 40 | 41 | UPDATE_INTERVAL = 30 42 | DEFAULT_TIMEOUT = 30 43 | SESSION_TIME = 59 44 | 45 | CONFIG_SCHEMA = vol.Schema({ 46 | DOMAIN: vol.Schema({ 47 | vol.Required(CONF_USERNAME): cv.string, 48 | vol.Required(CONF_PASSWORD): cv.string 49 | }) 50 | }, extra=vol.ALLOW_EXTRA) 51 | 52 | 53 | class EdpRedySession: 54 | """Representation of an http session to the service.""" 55 | 56 | def __init__(self, hass, username, password): 57 | """Init the session.""" 58 | self._username = username 59 | self._password = password 60 | self._session = None 61 | self._session_time = dt_util.utcnow() 62 | self._hass = hass 63 | self.modules_dict = {} 64 | self.values_dict = {} 65 | 66 | async def async_init_session(self): 67 | """Create a new http session.""" 68 | payload_auth = {'username': self._username, 69 | 'password': self._password, 70 | 'screenWidth': '1920', 'screenHeight': '1080'} 71 | 72 | try: 73 | # create session and fetch login page 74 | session = aiohttp_client.async_get_clientsession(self._hass) 75 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 76 | resp = await session.get(URL_LOGIN_PAGE) 77 | 78 | except (asyncio.TimeoutError, aiohttp.ClientError): 79 | _LOGGER.error("Error while accessing login page") 80 | return None 81 | 82 | if resp.status != 200: 83 | _LOGGER.error("Login page returned status code %s", resp.status) 84 | return None 85 | 86 | try: 87 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 88 | resp = await session.post(URL_LOGIN_PAGE, data=payload_auth) 89 | 90 | except (asyncio.TimeoutError, aiohttp.ClientError): 91 | _LOGGER.error("Error while doing login post") 92 | return None 93 | 94 | if resp.status != 200: 95 | _LOGGER.error("Login post returned status code %s", resp.status) 96 | return None 97 | 98 | return session 99 | 100 | async def async_logout(self): 101 | """Logout from the current session.""" 102 | _LOGGER.debug("Logout") 103 | 104 | try: 105 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 106 | resp = await self._session.get(URL_LOGOUT) 107 | 108 | except (asyncio.TimeoutError, aiohttp.ClientError): 109 | _LOGGER.error("Error while doing logout") 110 | return False 111 | 112 | if resp.status != 200: 113 | _LOGGER.error("Logout returned status code %s", resp.status) 114 | return False 115 | 116 | return True 117 | 118 | async def async_validate_session(self): 119 | """Check the current session and create a new one if needed.""" 120 | if self._session is not None: 121 | session_life = dt_util.utcnow() - self._session_time 122 | if session_life.total_seconds() < SESSION_TIME: 123 | """ Session valid, nothing to do """ 124 | return True 125 | 126 | """ Session is older than SESSION_TIME: logout """ 127 | await self.async_logout() 128 | self._session = None 129 | 130 | """ not valid, create new session """ 131 | self._session = await self.async_init_session() 132 | self._session_time = dt_util.utcnow() 133 | return True if self._session is not None else False 134 | 135 | async def async_fetch_active_power(self): 136 | """Fetch new data from the server.""" 137 | if not await self.async_validate_session(): 138 | return False 139 | 140 | try: 141 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 142 | resp = await self._session.post(URL_GET_ACTIVE_POWER) 143 | except (asyncio.TimeoutError, aiohttp.ClientError): 144 | _LOGGER.error("Error while getting active power") 145 | return False 146 | if resp.status != 200: 147 | _LOGGER.error("Getting active power returned status code %s", 148 | resp.status) 149 | return False 150 | 151 | active_power_str = await resp.text() 152 | _LOGGER.debug("Fetched Active Power:\n" + active_power_str) 153 | 154 | if active_power_str is None: 155 | return False 156 | 157 | try: 158 | updated_dict = json.loads(active_power_str) 159 | except (json.decoder.JSONDecodeError, TypeError): 160 | _LOGGER.error("Error parsing active power json. Received: \n %s", 161 | active_power_str) 162 | return False 163 | 164 | if "Body" not in updated_dict: 165 | return False 166 | if "ActivePower" not in updated_dict["Body"]: 167 | return False 168 | 169 | try: 170 | self.values_dict[ACTIVE_POWER_ID] = \ 171 | updated_dict["Body"]["ActivePower"] * 1000 172 | except ValueError: 173 | _LOGGER.error( 174 | "Could not parse value: ActivePower") 175 | self.values_dict[ACTIVE_POWER_ID] = None 176 | 177 | return True 178 | 179 | async def async_fetch_modules(self): 180 | """Fetch new data from the server.""" 181 | if not await self.async_validate_session(): 182 | return False 183 | 184 | try: 185 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 186 | resp = await self._session.post(URL_GET_SWITCH_MODULES, 187 | data={"filter": 1}) 188 | except (asyncio.TimeoutError, aiohttp.ClientError): 189 | _LOGGER.error("Error while getting switch modules") 190 | return False 191 | if resp.status != 200: 192 | _LOGGER.error("Getting switch modules returned status code %s", 193 | resp.status) 194 | return False 195 | 196 | modules_str = await resp.text() 197 | _LOGGER.debug("Fetched Modules:\n" + modules_str) 198 | 199 | if modules_str is None: 200 | return False 201 | 202 | try: 203 | updated_dict = json.loads(modules_str) 204 | except (json.decoder.JSONDecodeError, TypeError): 205 | _LOGGER.error("Error parsing modules json. Received: \n %s", 206 | modules_str) 207 | return False 208 | 209 | if "Body" not in updated_dict: 210 | return False 211 | if "Modules" not in updated_dict["Body"]: 212 | return False 213 | 214 | for module in updated_dict["Body"]["Modules"]: 215 | self.modules_dict[module['PKID']] = module 216 | 217 | return True 218 | 219 | async def async_update(self): 220 | """Get data from the server and update local structures.""" 221 | modules_success = await self.async_fetch_modules() 222 | active_power_success = await self.async_fetch_active_power() 223 | 224 | return modules_success and active_power_success 225 | 226 | async def async_set_state_var(self, json_payload): 227 | """Call SetStateVar API on the server.""" 228 | if not await self.async_validate_session(): 229 | return False 230 | 231 | _LOGGER.debug("Calling %s with: %s", URL_SET_STATE_VAR, 232 | str(json_payload)) 233 | 234 | try: 235 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): 236 | resp = await self._session.post(URL_SET_STATE_VAR, 237 | json=json_payload) 238 | except (asyncio.TimeoutError, aiohttp.ClientError): 239 | _LOGGER.error("Error while setting state var") 240 | return False 241 | if resp.status != 200: 242 | _LOGGER.error("Setting state var returned status code %s", 243 | resp.status) 244 | return False 245 | 246 | return True 247 | 248 | 249 | async def async_setup(hass, config): 250 | """Set up the EDP re:dy component.""" 251 | session = EdpRedySession(hass, config[DOMAIN][CONF_USERNAME], 252 | config[DOMAIN][CONF_PASSWORD]) 253 | hass.data[EDP_REDY] = session 254 | platform_loaded = False 255 | 256 | async def async_update_and_sched(time): 257 | update_success = await session.async_update() 258 | 259 | if update_success: 260 | dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) 261 | 262 | nonlocal platform_loaded 263 | if not platform_loaded: 264 | for component in ['sensor', 'switch']: 265 | await discovery.async_load_platform(hass, component, 266 | DOMAIN, {}, config) 267 | platform_loaded = True 268 | 269 | # schedule next update 270 | async_track_point_in_time(hass, async_update_and_sched, 271 | time + timedelta(seconds=UPDATE_INTERVAL)) 272 | 273 | async def start_component(event): 274 | _LOGGER.debug("Starting updates") 275 | await async_update_and_sched(dt_util.utcnow()) 276 | 277 | # only start fetching data after HA boots to prevent delaying the boot 278 | # process 279 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) 280 | 281 | return True 282 | 283 | 284 | class EdpRedyDevice(Entity): 285 | """Representation a base re:dy device.""" 286 | 287 | def __init__(self, session, device_id, name): 288 | """Initialize the device.""" 289 | self._session = session 290 | self._state = None 291 | self._is_available = True 292 | self._device_state_attributes = {} 293 | self._id = device_id 294 | self._unique_id = device_id 295 | self._name = name if len(name) > 0 else device_id 296 | 297 | async def async_added_to_hass(self): 298 | """Subscribe to the data updates topic.""" 299 | dispatcher.async_dispatcher_connect( 300 | self.hass, DATA_UPDATE_TOPIC, self._data_updated) 301 | 302 | @property 303 | def name(self): 304 | """Return the name of the device.""" 305 | return self._name 306 | 307 | @property 308 | def unique_id(self) -> str: 309 | """Return a unique ID.""" 310 | return self._unique_id 311 | 312 | @property 313 | def available(self): 314 | """Return True if entity is available.""" 315 | return self._is_available 316 | 317 | @property 318 | def should_poll(self): 319 | """Return the polling state. No polling needed.""" 320 | return False 321 | 322 | @property 323 | def device_state_attributes(self): 324 | """Return the state attributes.""" 325 | return self._device_state_attributes 326 | 327 | @callback 328 | def _data_updated(self): 329 | """Update state, trigger updates.""" 330 | self.async_schedule_update_ha_state(True) 331 | 332 | def _parse_data(self, data): 333 | """Parse data received from the server.""" 334 | if "OutOfOrder" in data: 335 | try: 336 | self._is_available = not data["OutOfOrder"] 337 | except ValueError: 338 | _LOGGER.error( 339 | "Could not parse OutOfOrder for %s", self._id) 340 | self._is_available = False 341 | -------------------------------------------------------------------------------- /edp_redy/sensor/edp_redy.py: -------------------------------------------------------------------------------- 1 | """Support for EDP re:dy sensors.""" 2 | import logging 3 | 4 | from homeassistant.helpers.entity import Entity 5 | 6 | try: 7 | from homeassistant.components.edp_redy import (EdpRedyDevice, EDP_REDY, 8 | ACTIVE_POWER_ID) 9 | except ImportError: 10 | from custom_components.edp_redy import (EdpRedyDevice, EDP_REDY, 11 | ACTIVE_POWER_ID) 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | # Load power in watts (W) 16 | ATTR_ACTIVE_POWER = 'active_power' 17 | 18 | 19 | def setup_platform(hass, config, add_devices, discovery_info=None): 20 | """Perform the setup for re:dy devices.""" 21 | session = hass.data[EDP_REDY] 22 | devices = [] 23 | 24 | """ Create sensors for modules """ 25 | for device_pkid, device_json in session.modules_dict.items(): 26 | if "HA_POWER_METER" not in device_json["Capabilities"]: 27 | continue 28 | devices.append(EdpRedyModuleSensor(session, device_json)) 29 | 30 | """ Create a sensor for global active power """ 31 | devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", 32 | "mdi:flash", "W")) 33 | 34 | add_devices(devices) 35 | 36 | 37 | class EdpRedySensor(EdpRedyDevice, Entity): 38 | """Representation of a EDP re:dy generic sensor.""" 39 | 40 | def __init__(self, session, sensor_id, name, icon, unit): 41 | """Initialize the sensor.""" 42 | EdpRedyDevice.__init__(self, session, sensor_id, name) 43 | 44 | self._icon = icon 45 | self._unit = unit 46 | 47 | self._update_state() 48 | 49 | @property 50 | def state(self): 51 | """Return the state of the sensor.""" 52 | return self._state 53 | 54 | @property 55 | def icon(self): 56 | """Return the icon to use in the frontend.""" 57 | return self._icon 58 | 59 | @property 60 | def unit_of_measurement(self): 61 | """Return the unit of measurement of this sensor.""" 62 | return self._unit 63 | 64 | def _data_updated(self): 65 | self._update_state() 66 | super()._data_updated() 67 | 68 | def _update_state(self): 69 | if self._id in self._session.values_dict: 70 | self._state = self._session.values_dict[self._id] 71 | self._is_available = True 72 | else: 73 | self._is_available = False 74 | 75 | 76 | class EdpRedyModuleSensor(EdpRedyDevice, Entity): 77 | """Representation of a EDP re:dy module sensor.""" 78 | 79 | def __init__(self, session, device_json): 80 | """Initialize the sensor.""" 81 | EdpRedyDevice.__init__(self, session, device_json['PKID'], 82 | "Power {0}".format(device_json['Name'])) 83 | 84 | self._parse_data(device_json) 85 | 86 | @property 87 | def state(self): 88 | """Return the state of the sensor.""" 89 | return self._state 90 | 91 | @property 92 | def icon(self): 93 | """Return the icon to use in the frontend.""" 94 | return "mdi:flash" 95 | 96 | @property 97 | def unit_of_measurement(self): 98 | """Return the unit of measurement of this sensor.""" 99 | return 'W' 100 | 101 | def _data_updated(self): 102 | if self._id in self._session.modules_dict: 103 | device_json = self._session.modules_dict[self._id] 104 | self._parse_data(device_json) 105 | else: 106 | self._is_available = False 107 | 108 | super()._data_updated() 109 | 110 | def _parse_data(self, data): 111 | """Parse data received from the server.""" 112 | super()._parse_data(data) 113 | 114 | _LOGGER.debug("Sensor data: " + str(data)) 115 | 116 | for state_var in data["StateVars"]: 117 | if state_var["Name"] == "ActivePower": 118 | try: 119 | self._state = float(state_var["Value"]) * 1000 120 | except ValueError: 121 | _LOGGER.error("Could not parse power for %s", self._id) 122 | self._state = 0 123 | self._is_available = False 124 | -------------------------------------------------------------------------------- /edp_redy/switch/edp_redy.py: -------------------------------------------------------------------------------- 1 | """Support for EDP re:dy plugs/switches.""" 2 | import logging 3 | 4 | try: 5 | from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY 6 | except ImportError: 7 | from custom_components.edp_redy import EdpRedyDevice, EDP_REDY 8 | 9 | from homeassistant.components.switch import SwitchDevice 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # Load power in watts (W) 14 | ATTR_ACTIVE_POWER = 'active_power' 15 | 16 | 17 | def setup_platform(hass, config, add_devices, discovery_info=None): 18 | """Perform the setup for re:dy devices.""" 19 | session = hass.data[EDP_REDY] 20 | devices = [] 21 | for device_pkid, device_json in session.modules_dict.items(): 22 | if "HA_SWITCH" not in device_json["Capabilities"]: 23 | continue 24 | devices.append(EdpRedySwitch(session, device_json)) 25 | 26 | add_devices(devices) 27 | 28 | 29 | class EdpRedySwitch(EdpRedyDevice, SwitchDevice): 30 | """Representation of a Edp re:dy switch (plugs, switches, etc).""" 31 | 32 | def __init__(self, session, device_json): 33 | """Initialize the switch.""" 34 | EdpRedyDevice.__init__(self, session, device_json['PKID'], 35 | device_json['Name']) 36 | 37 | self._active_power = None 38 | 39 | self._parse_data(device_json) 40 | 41 | @property 42 | def icon(self): 43 | """Return the icon to use in the frontend.""" 44 | return 'mdi:power-plug' 45 | 46 | @property 47 | def is_on(self): 48 | """Return true if it is on.""" 49 | return self._state 50 | 51 | @property 52 | def device_state_attributes(self): 53 | """Return the state attributes.""" 54 | if self._active_power is not None: 55 | attrs = {ATTR_ACTIVE_POWER: self._active_power} 56 | else: 57 | attrs = {} 58 | attrs.update(super().device_state_attributes) 59 | return attrs 60 | 61 | async def async_turn_on(self, **kwargs): 62 | """Turn the switch on.""" 63 | if await self._async_send_state_cmd(True): 64 | self._state = True 65 | self.schedule_update_ha_state() 66 | 67 | async def async_turn_off(self, **kwargs): 68 | """Turn the switch off.""" 69 | if await self._async_send_state_cmd(False): 70 | self._state = False 71 | self.schedule_update_ha_state() 72 | 73 | async def _async_send_state_cmd(self, state): 74 | state_json = {"devModuleId": self._id, "key": "RelayState", 75 | "value": state} 76 | return await self._session.async_set_state_var(state_json) 77 | 78 | def _data_updated(self): 79 | if self._id in self._session.modules_dict: 80 | device_json = self._session.modules_dict[self._id] 81 | self._parse_data(device_json) 82 | else: 83 | self._is_available = False 84 | 85 | super()._data_updated() 86 | 87 | def _parse_data(self, data): 88 | """Parse data received from the server.""" 89 | super()._parse_data(data) 90 | 91 | for state_var in data["StateVars"]: 92 | if state_var["Name"] == "RelayState": 93 | self._state = True if state_var["Value"] == "true" \ 94 | else False 95 | elif state_var["Name"] == "ActivePower": 96 | try: 97 | self._active_power = float(state_var["Value"]) * 1000 98 | except ValueError: 99 | _LOGGER.error("Could not parse power for %s", self._id) 100 | self._active_power = None 101 | -------------------------------------------------------------------------------- /others/README.md: -------------------------------------------------------------------------------- 1 | ### edp_redy_local: 2 | Copy the edp_redy_local folder to your custom_components folder and add the following configuration: 3 | 4 | ``` 5 | sensor: 6 | - platform: edp_redy_local 7 | host: 192.168.1.2 8 | update_interval: 10 9 | ``` 10 | -------------------------------------------------------------------------------- /others/device_tracker_sensor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abmantis/homeassistant-custom-components/2d340f45ee479d28a9bb7f8b50356586938e8532/others/device_tracker_sensor/__init__.py -------------------------------------------------------------------------------- /others/device_tracker_sensor/binary_sensor.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Creates a sensor that creates binary sensors from device_tracker devices 3 | # """ 4 | import asyncio 5 | import logging 6 | 7 | import voluptuous as vol 8 | 9 | 10 | from homeassistant.core import callback 11 | from homeassistant.components.binary_sensor import BinarySensorDevice, \ 12 | ENTITY_ID_FORMAT, PLATFORM_SCHEMA 13 | from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE 14 | from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_ENTITIES, 15 | EVENT_HOMEASSISTANT_START) 16 | from homeassistant.exceptions import TemplateError 17 | import homeassistant.helpers.config_validation as cv 18 | from homeassistant.helpers.entity import Entity, async_generate_entity_id 19 | from homeassistant.helpers.event import async_track_state_change 20 | from homeassistant.helpers import template as template_helper 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 25 | vol.Required(CONF_ENTITIES): cv.entity_ids 26 | }) 27 | 28 | @asyncio.coroutine 29 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 30 | """Set up the sensors.""" 31 | _LOGGER.info("Starting device tracker sensor") 32 | sensors = [] 33 | 34 | for device in config[CONF_ENTITIES]: 35 | 36 | state_template = "{{{{ is_state('{0}', 'home') }}}}".format(device) 37 | _LOGGER.debug("Applying template: %s", state_template) 38 | 39 | state_template = template_helper.Template(state_template) 40 | state_template.hass = hass 41 | 42 | device_state = hass.states.get(device) 43 | if device_state is not None: 44 | friendly_name = device_state.attributes.get(ATTR_FRIENDLY_NAME) 45 | source_type = device_state.attributes.get(ATTR_SOURCE_TYPE) 46 | else: 47 | friendly_name = None 48 | source_type = None 49 | 50 | if friendly_name is None: 51 | friendly_name = device.split(".", 1)[1] 52 | 53 | sensors.append( 54 | DeviceTrackerSensor( 55 | hass, 56 | "device_tracker_{0}".format(device.split(".", 1)[1]), 57 | friendly_name, 58 | source_type, 59 | state_template, 60 | device) 61 | ) 62 | if not sensors: 63 | _LOGGER.error("No sensors added") 64 | return False 65 | 66 | async_add_devices(sensors) 67 | return True 68 | 69 | 70 | class DeviceTrackerSensor(BinarySensorDevice): 71 | """Representation of a Device Tracker Sensor.""" 72 | 73 | def __init__(self, hass, device_id, friendly_name, source_type, 74 | state_template, entity_id): 75 | """Initialize the sensor.""" 76 | self.hass = hass 77 | self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, 78 | hass=hass) 79 | self._name = friendly_name 80 | self._source_type = source_type 81 | self._template = state_template 82 | self._state = False 83 | self._entity = entity_id 84 | 85 | @asyncio.coroutine 86 | def async_added_to_hass(self): 87 | """Register callbacks.""" 88 | 89 | @callback 90 | def template_sensor_state_listener(entity, old_state, new_state): 91 | """Handle device state changes.""" 92 | self.hass.async_add_job(self.async_update_ha_state(True)) 93 | 94 | @callback 95 | def template_sensor_startup(event): 96 | """Update on startup.""" 97 | async_track_state_change( 98 | self.hass, self._entity, template_sensor_state_listener) 99 | 100 | self.hass.async_add_job(self.async_update_ha_state(True)) 101 | 102 | self.hass.bus.async_listen_once( 103 | EVENT_HOMEASSISTANT_START, template_sensor_startup) 104 | 105 | @property 106 | def device_state_attributes(self): 107 | """Return the state attributes.""" 108 | return {ATTR_SOURCE_TYPE: self._source_type} 109 | 110 | @property 111 | def should_poll(self): 112 | """No polling needed.""" 113 | return False 114 | 115 | @property 116 | def is_on(self): 117 | """Return true if sensor is on.""" 118 | return self._state 119 | 120 | @property 121 | def name(self): 122 | """Return the entity name.""" 123 | return self._name 124 | 125 | # @property 126 | # def device_class(self): 127 | # """Return the class of binary sensor.""" 128 | # return self._device_class 129 | 130 | @asyncio.coroutine 131 | def async_update(self): 132 | """Update the sensor state.""" 133 | _LOGGER.debug('Updating device_tracker_sensor sensor') 134 | 135 | entity_state = self.hass.states.get(self._entity) 136 | if entity_state is not None: 137 | device_friendly_name = entity_state.attributes.get( 138 | ATTR_FRIENDLY_NAME) 139 | self._source_type = entity_state.attributes.get(ATTR_SOURCE_TYPE) 140 | else: 141 | device_friendly_name = None 142 | self._source_type = None 143 | 144 | if device_friendly_name is not None: 145 | self._name = device_friendly_name 146 | 147 | try: 148 | self._state = self._template.async_render().lower() == 'true' 149 | except TemplateError as ex: 150 | if ex.args and ex.args[0].startswith( 151 | "UndefinedError: 'None' has no attribute"): 152 | # Common during HA startup - so just a warning 153 | _LOGGER.warning('Could not render attribute sensor for %s,' 154 | ' the state is unknown.', self._entity) 155 | return 156 | self._state = False 157 | _LOGGER.error('Could not attribute sensor for %s: %s', 158 | self._entity, ex) 159 | -------------------------------------------------------------------------------- /others/edp_redy_local/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abmantis/homeassistant-custom-components/2d340f45ee479d28a9bb7f8b50356586938e8532/others/edp_redy_local/__init__.py -------------------------------------------------------------------------------- /others/edp_redy_local/sensor.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Creates sensors for EDP Re:dy power readings 3 | # """ 4 | 5 | import aiohttp 6 | import asyncio 7 | import async_timeout 8 | import json 9 | import logging 10 | from datetime import timedelta 11 | 12 | import voluptuous as vol 13 | 14 | from homeassistant.core import callback 15 | from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_HOST, 16 | EVENT_HOMEASSISTANT_START, STATE_UNKNOWN) 17 | import homeassistant.helpers.config_validation as cv 18 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 19 | from homeassistant.helpers.entity import Entity, async_generate_entity_id 20 | from homeassistant.helpers.event import (async_track_state_change, 21 | async_track_point_in_time) 22 | from homeassistant.helpers.config_validation import PLATFORM_SCHEMA 23 | from homeassistant.helpers import template as template_helper 24 | from homeassistant.util import dt as dt_util 25 | 26 | from html.parser import HTMLParser 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | DOMAIN = 'edp_redy_local' 31 | ATTR_LAST_COMMUNICATION = 'last_communication' 32 | CONF_UPDATE_INTERVAL = 'update_interval' 33 | DEFAULT_TIMEOUT = 10 34 | 35 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 36 | vol.Required(CONF_HOST): cv.string, 37 | vol.Optional(CONF_UPDATE_INTERVAL, default=30): cv.positive_int, 38 | }) 39 | 40 | 41 | @asyncio.coroutine 42 | def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 43 | class RedyHTMLParser(HTMLParser): 44 | def __init__(self): 45 | super().__init__() 46 | self._json = '' 47 | 48 | def handle_data(self, data): 49 | if data.find('REDYMETER') != -1: 50 | self._json = data 51 | 52 | def json(self): 53 | return self._json 54 | 55 | host = config[CONF_HOST] 56 | url = 'http://{}:1234/api/devices'.format(host) 57 | 58 | sensors = {} 59 | new_sensors_list = [] 60 | 61 | def load_sensor(sensor_id, name, power, last_communication): 62 | if sensor_id in sensors: 63 | sensors[sensor_id].update_data(power, last_communication) 64 | return 65 | 66 | # create new sensor 67 | sensor = EdpRedyLocalSensor(sensor_id, name, power, last_communication) 68 | sensors[sensor_id] = sensor 69 | new_sensors_list.append(sensor) 70 | 71 | def parse_nodes(json_nodes): 72 | for node in json_nodes: 73 | if "ID" not in node: 74 | continue 75 | if "NAME" not in node: 76 | continue 77 | if "EMETER:POWER_APLUS" not in node: 78 | continue 79 | 80 | node_id = node["ID"] 81 | node_name = node["NAME"] 82 | node_power = node["EMETER:POWER_APLUS"] 83 | load_sensor(node_id, node_name, node_power, None) 84 | 85 | def parse_type(json, type_tag): 86 | if type_tag not in json: 87 | return 88 | 89 | for device in json[type_tag]: 90 | if "NODES" not in device: 91 | continue 92 | parse_nodes(device["NODES"]) 93 | 94 | def parse_json(json): 95 | parse_type(json, "REDYMETER") 96 | parse_type(json, "ZBENDPOINT") 97 | 98 | if "EDPBOX" in json: 99 | edp_box_json = json["EDPBOX"] 100 | if len(edp_box_json) > 0: 101 | edpbox_data = edp_box_json[0] 102 | edpbox_id = edpbox_data["SMARTMETER_ID"] 103 | edpbox_power = edpbox_data["EMETER:POWER_APLUS"] 104 | edpbox_last_comm = edpbox_data["LAST_COMMUNICATION"] 105 | load_sensor(edpbox_id, "Smart Meter", edpbox_power, edpbox_last_comm) 106 | 107 | @asyncio.coroutine 108 | def async_update(): 109 | """Fetch data from the redy box and update sensors.""" 110 | 111 | try: 112 | # get the data from the box 113 | session = async_get_clientsession(hass) 114 | with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop): 115 | resp = yield from session.get(url) 116 | 117 | except (asyncio.TimeoutError, aiohttp.ClientError): 118 | _LOGGER.error("Error while accessing: %s", url) 119 | return 120 | 121 | if resp.status != 200: 122 | _LOGGER.error("%s not available", url) 123 | return 124 | 125 | data_html = yield from resp.text() 126 | 127 | try: 128 | html_parser = RedyHTMLParser() 129 | html_parser.feed(data_html) 130 | html_parser.close() 131 | html_json = html_parser.json() 132 | j = json.loads(html_json) 133 | 134 | new_sensors_list.clear() 135 | parse_json(j) 136 | if len(new_sensors_list) > 0: 137 | async_add_entities(new_sensors_list) 138 | 139 | except Exception as error: 140 | _LOGGER.error("Failed to load data from redy box: %s", error) 141 | 142 | @asyncio.coroutine 143 | def async_update_and_sched(time): 144 | yield from async_update() 145 | # schedule next update 146 | async_track_point_in_time(hass, async_update_and_sched, 147 | time + timedelta( 148 | seconds=config[CONF_UPDATE_INTERVAL])) 149 | 150 | @asyncio.coroutine 151 | def start_component(event): 152 | _LOGGER.debug("Starting updates") 153 | yield from async_update_and_sched(dt_util.utcnow()) 154 | 155 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) 156 | 157 | 158 | class EdpRedyLocalSensor(Entity): 159 | """Representation of a sensor.""" 160 | 161 | def __init__(self, node_id, name, power, last_communication): 162 | """Set up sensor and add update callback to get data from websocket.""" 163 | self._id = node_id 164 | self._name = 'Power {0}'.format(name) 165 | self._power = float(power)*1000 if power else STATE_UNKNOWN 166 | self._last_comm = last_communication 167 | 168 | def update_data(self, power, last_communication): 169 | """Update the sensor's state.""" 170 | self._power = float(power)*1000 if power else STATE_UNKNOWN 171 | self._last_comm = last_communication 172 | self.async_schedule_update_ha_state() 173 | 174 | @property 175 | def state(self): 176 | """Return the state of the sensor.""" 177 | return self._power 178 | 179 | @property 180 | def name(self): 181 | """Return the name of the sensor.""" 182 | return self._name 183 | 184 | @property 185 | def unique_id(self): 186 | """Return a unique identifier for this sensor.""" 187 | return self._id 188 | 189 | @property 190 | def icon(self): 191 | """Return the icon to use in the frontend.""" 192 | return "mdi:flash" 193 | 194 | # @property 195 | # def icon(self): 196 | # """Return the icon to use in the frontend.""" 197 | # return self._sensor.sensor_icon 198 | 199 | @property 200 | def unit_of_measurement(self): 201 | """Return the unit of measurement of this sensor.""" 202 | return 'W' 203 | 204 | @property 205 | def should_poll(self): 206 | """No polling needed.""" 207 | return False 208 | 209 | @property 210 | def device_state_attributes(self): 211 | """Return the state attributes of the sensor.""" 212 | if self._last_comm: 213 | attr = { 214 | ATTR_LAST_COMMUNICATION: self._last_comm, 215 | } 216 | return attr 217 | return None 218 | -------------------------------------------------------------------------------- /others/timed_state_infer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abmantis/homeassistant-custom-components/2d340f45ee479d28a9bb7f8b50356586938e8532/others/timed_state_infer/__init__.py -------------------------------------------------------------------------------- /others/timed_state_infer/binary_sensor.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Infers its state from the current state and duration of other sensors. 3 | # """ 4 | import asyncio 5 | import logging 6 | from datetime import timedelta 7 | import voluptuous as vol 8 | 9 | from homeassistant.components.binary_sensor import BinarySensorDevice 10 | from homeassistant.const import CONF_ENTITY_ID, STATE_UNKNOWN, CONF_NAME 11 | import homeassistant.helpers.config_validation as cv 12 | from homeassistant.core import callback 13 | from homeassistant.helpers.config_validation import PLATFORM_SCHEMA 14 | from homeassistant.helpers.event import async_track_state_change,\ 15 | async_track_point_in_time 16 | from homeassistant.util import dt as dt_util 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | CONF_TIME_ON = 'seconds_on' 21 | CONF_TIME_OFF = 'seconds_off' 22 | CONF_VALUE_ON = 'value_on' 23 | CONF_VALUE_OFF = 'value_off' 24 | DEFAULT_NAME = "Timed State Infer Binary Sensor" 25 | 26 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 27 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 28 | vol.Required(CONF_ENTITY_ID): cv.entity_id, 29 | vol.Required(CONF_TIME_ON): cv.positive_int, 30 | vol.Required(CONF_TIME_OFF): cv.positive_int, 31 | vol.Required(CONF_VALUE_ON): vol.Coerce(float), 32 | vol.Required(CONF_VALUE_OFF): vol.Coerce(float) 33 | }) 34 | 35 | 36 | @asyncio.coroutine 37 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 38 | async_add_devices([TimedStateInferBinarySensor(hass, config[CONF_NAME], 39 | config[CONF_ENTITY_ID], 40 | config[CONF_TIME_ON], 41 | config[CONF_TIME_OFF], 42 | config[CONF_VALUE_ON], 43 | config[CONF_VALUE_OFF])]) 44 | 45 | 46 | class TimedStateInferBinarySensor(BinarySensorDevice): 47 | """Representation of a sensor.""" 48 | 49 | def __init__(self, hass, name, observed_entity_id, time_on, time_off, 50 | value_on, value_off): 51 | self._hass = hass 52 | self._name = name 53 | self._observed_entity_id = observed_entity_id 54 | self._time_on = timedelta(seconds=time_on) 55 | self._time_off = timedelta(seconds=time_off) 56 | self._value_on = value_on 57 | self._value_off = value_off 58 | self._is_on = False 59 | self._pending = False 60 | self._pending_since = None 61 | 62 | @property 63 | def name(self): 64 | """Return the name of the sensor.""" 65 | return self._name 66 | 67 | @property 68 | def should_poll(self): 69 | """No polling needed.""" 70 | return False 71 | 72 | @property 73 | def is_on(self): 74 | """Return true if sensor is on.""" 75 | return self._is_on 76 | 77 | @asyncio.coroutine 78 | def async_added_to_hass(self): 79 | """Call when entity about to be added.""" 80 | 81 | @callback 82 | def async_sensor_state_listener(entity, old_state, new_state): 83 | """Handle sensor state changes.""" 84 | self.update_state(new_state.state) 85 | 86 | async_track_state_change(self._hass, self._observed_entity_id, 87 | async_sensor_state_listener) 88 | 89 | # update the sensor when added to hass 90 | yield from self.async_pending_expired(dt_util.utcnow()) 91 | 92 | @asyncio.coroutine 93 | def async_pending_expired(self, time): 94 | device_state = self.hass.states.get(self._observed_entity_id) 95 | if device_state is None: 96 | return 97 | 98 | self.update_state(device_state.state) 99 | 100 | def update_state(self, observed_entity_state): 101 | if observed_entity_state == STATE_UNKNOWN: 102 | return 103 | 104 | try: 105 | obs_value = float(observed_entity_state) 106 | except ValueError: 107 | _LOGGER.warning("Value cannot be processed as a number: %s", 108 | observed_entity_state) 109 | return 110 | 111 | """If we are already in the correct state, no need to do anything""" 112 | if self._is_on: 113 | if obs_value > self._value_off: 114 | self._pending = False 115 | return 116 | else: 117 | if obs_value < self._value_on: 118 | self._pending = False 119 | return 120 | 121 | if self._pending: 122 | time_pending = dt_util.utcnow() - self._pending_since 123 | if self._is_on: 124 | time_remaining = self._time_off - time_pending 125 | else: 126 | time_remaining = self._time_on - time_pending 127 | 128 | if time_remaining.total_seconds() <= 0: 129 | self._is_on = not self._is_on 130 | self._pending = False 131 | self.schedule_update_ha_state() 132 | else: 133 | # enter pending mode (start counting time to change state) 134 | self._pending_since = dt_util.utcnow() 135 | self._pending = True 136 | 137 | time_to_expire = dt_util.utcnow() + ( 138 | self._time_off if self._is_on else self._time_on) 139 | 140 | async_track_point_in_time(self._hass, self.async_pending_expired, 141 | time_to_expire) 142 | -------------------------------------------------------------------------------- /whirlpool/README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED! This is now integrated officially in Home Assistant. Please use the Home Assistant Integrations UI instead. 2 | 3 | # Component for Whirlpool 6th Sense appliances 4 | At the moment, only air conditioners are supported. More appliances may be integrated in the future. 5 | 6 | # How to use: 7 | 8 | - Create a folder named `whirlpool` inside your `custom_components` folder and copy all files in this repository directory the `whirlpool` folder. 9 | 10 | - Add to configuration.yaml: 11 | 12 | ``` 13 | whirlpool: 14 | username: your@mail.com 15 | password: YoUrPaSsWoRd 16 | ``` 17 | -------------------------------------------------------------------------------- /whirlpool/__init__.py: -------------------------------------------------------------------------------- 1 | """Whirlpool's Sixth Sense integration.""" 2 | 3 | from whirlpool.auth import Auth 4 | import logging 5 | import voluptuous as vol 6 | 7 | import homeassistant.helpers.config_validation as cv 8 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | DOMAIN = "whirlpool" 13 | 14 | CONFIG_SCHEMA = vol.Schema( 15 | { 16 | DOMAIN: vol.Schema( 17 | { 18 | vol.Required(CONF_USERNAME): cv.string, 19 | vol.Required(CONF_PASSWORD): cv.string, 20 | } 21 | ) 22 | }, 23 | extra=vol.ALLOW_EXTRA, 24 | ) 25 | 26 | 27 | async def async_setup(hass, config): 28 | username = config[DOMAIN][CONF_USERNAME] 29 | password = config[DOMAIN][CONF_PASSWORD] 30 | auth = Auth(username, password) 31 | await auth.load_auth_file() 32 | 33 | hass.data[DOMAIN] = {"auth": auth} 34 | 35 | hass.helpers.discovery.load_platform("climate", DOMAIN, {}, config) 36 | 37 | return True -------------------------------------------------------------------------------- /whirlpool/climate.py: -------------------------------------------------------------------------------- 1 | """Platform for climate integration.""" 2 | from whirlpool.aircon import Aircon, Mode as AirconMode, FanSpeed as AirconFanSpeed 3 | from whirlpool.auth import Auth 4 | 5 | import logging 6 | 7 | from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS 8 | from homeassistant.components.climate import ClimateEntity 9 | from homeassistant.components.climate.const import ( 10 | ATTR_FAN_MODE, 11 | ATTR_HUMIDITY, 12 | ATTR_HVAC_MODE, 13 | ATTR_PRESET_MODE, 14 | ATTR_SWING_MODE, 15 | FAN_AUTO, 16 | FAN_LOW, 17 | FAN_MEDIUM, 18 | FAN_OFF, 19 | FAN_HIGH, 20 | HVAC_MODE_COOL, 21 | HVAC_MODE_FAN_ONLY, 22 | HVAC_MODE_HEAT, 23 | HVAC_MODE_OFF, 24 | SUPPORT_FAN_MODE, 25 | SUPPORT_SWING_MODE, 26 | SUPPORT_TARGET_TEMPERATURE, 27 | SWING_HORIZONTAL, 28 | SWING_OFF, 29 | ) 30 | 31 | from . import DOMAIN 32 | 33 | _LOGGER = logging.getLogger(__name__) 34 | 35 | 36 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 37 | """Set up the sensor platform.""" 38 | # We only want this platform to be set up via discovery. 39 | if discovery_info is None: 40 | return 41 | auth: Auth = hass.data[DOMAIN]["auth"] 42 | said_list = auth.get_said_list() 43 | if not said_list: 44 | return 45 | devices = [] 46 | for key in range(len(said_list)): 47 | aircon = AirConEntity(said_list[key], auth) 48 | await aircon._async_connect() 49 | devices.append(aircon) 50 | 51 | async_add_entities(devices, True) 52 | 53 | 54 | class AirConEntity(ClimateEntity): 55 | """Representation of an air conditioner.""" 56 | 57 | def __init__(self, said, auth: Auth): 58 | """Initialize the entity.""" 59 | self._aircon = Aircon(auth, said, self.schedule_update_ha_state) 60 | 61 | self._supported_features = SUPPORT_TARGET_TEMPERATURE 62 | self._supported_features |= SUPPORT_FAN_MODE 63 | self._supported_features |= SUPPORT_SWING_MODE 64 | 65 | async def _async_connect(self): 66 | """Connect aircon to the cloud.""" 67 | await self._aircon.connect() 68 | 69 | @property 70 | def min_temp(self) -> float: 71 | """Return the minimum temperature.""" 72 | return 16 73 | 74 | @property 75 | def max_temp(self) -> float: 76 | """Return the maximum temperature.""" 77 | return 30 78 | 79 | @property 80 | def supported_features(self): 81 | """Return the list of supported features.""" 82 | return self._supported_features 83 | 84 | @property 85 | def name(self): 86 | """Return the name of the aircon.""" 87 | return self._aircon._said # TODO: return user-given-name from the API 88 | 89 | @property 90 | def unique_id(self): 91 | """Return a unique ID.""" 92 | return self._aircon._said 93 | 94 | @property 95 | def available(self) -> bool: 96 | """Return True if entity is available.""" 97 | return self._aircon.get_online() 98 | 99 | @property 100 | def temperature_unit(self): 101 | """Return the unit of measurement which this thermostat uses.""" 102 | return TEMP_CELSIUS 103 | 104 | @property 105 | def current_temperature(self): 106 | """Return the current temperature.""" 107 | return self._aircon.get_current_temp() 108 | 109 | @property 110 | def target_temperature(self): 111 | """Return the temperature we try to reach.""" 112 | return self._aircon.get_temp() 113 | 114 | @property 115 | def target_temperature_step(self): 116 | """Return the supported step of target temperature.""" 117 | return 1 118 | 119 | async def async_set_temperature(self, **kwargs): 120 | """Set new target temperature.""" 121 | await self._aircon.set_temp(kwargs.get(ATTR_TEMPERATURE)) 122 | 123 | @property 124 | def current_humidity(self): 125 | """Return the current humidity.""" 126 | return self._aircon.get_current_humidity() 127 | 128 | @property 129 | def target_humidity(self): 130 | """Return the humidity we try to reach.""" 131 | return self._aircon.get_humidity() 132 | 133 | @property 134 | def target_humidity_step(self): 135 | """Return the supported step of target humidity.""" 136 | return 10 137 | 138 | async def async_set_humidity(self, **kwargs): 139 | """Set new target humidity.""" 140 | await self._aircon.set_humidity(kwargs.get(ATTR_HUMIDITY)) 141 | 142 | @property 143 | def hvac_modes(self): 144 | """Return the list of available operation modes.""" 145 | return [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF] 146 | 147 | @property 148 | def hvac_mode(self): 149 | """Return current operation ie. heat, cool, fan.""" 150 | if not self._aircon.get_power_on(): 151 | return HVAC_MODE_OFF 152 | 153 | mode: AirconMode = self._aircon.get_mode() 154 | if mode == AirconMode.Cool: 155 | return HVAC_MODE_COOL 156 | elif mode == AirconMode.Heat: 157 | return HVAC_MODE_HEAT 158 | elif mode == AirconMode.Fan: 159 | return HVAC_MODE_FAN_ONLY 160 | 161 | return None 162 | 163 | async def async_set_hvac_mode(self, hvac_mode): 164 | """Set HVAC mode.""" 165 | if hvac_mode == HVAC_MODE_OFF: 166 | await self._aircon.set_power_on(False) 167 | 168 | mode = None 169 | if hvac_mode == HVAC_MODE_COOL: 170 | mode = AirconMode.Cool 171 | elif hvac_mode == HVAC_MODE_HEAT: 172 | mode = AirconMode.Heat 173 | elif hvac_mode == HVAC_MODE_FAN_ONLY: 174 | mode = AirconMode.Fan 175 | 176 | if not mode: 177 | return 178 | 179 | await self._aircon.set_mode(mode) 180 | if not self._aircon.get_power_on(): 181 | await self._aircon.set_power_on(True) 182 | 183 | @property 184 | def fan_modes(self): 185 | """List of available fan modes.""" 186 | return [FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] 187 | 188 | @property 189 | def fan_mode(self): 190 | """Return the fan setting.""" 191 | fanspeed = self._aircon.get_fanspeed() 192 | if fanspeed == AirconFanSpeed.Auto: 193 | return FAN_AUTO 194 | elif fanspeed == AirconFanSpeed.Low: 195 | return FAN_LOW 196 | elif fanspeed == AirconFanSpeed.Medium: 197 | return FAN_MEDIUM 198 | elif fanspeed == AirconFanSpeed.High: 199 | return FAN_HIGH 200 | return FAN_OFF 201 | 202 | async def async_set_fan_mode(self, fan_mode): 203 | """Set fan mode.""" 204 | fanspeed = None 205 | if fan_mode == FAN_AUTO: 206 | fanspeed = AirconFanSpeed.Auto 207 | elif fan_mode == FAN_LOW: 208 | fanspeed = AirconFanSpeed.Low 209 | elif fan_mode == FAN_MEDIUM: 210 | fanspeed = AirconFanSpeed.Medium 211 | elif fan_mode == FAN_HIGH: 212 | fanspeed = AirconFanSpeed.High 213 | if not fanspeed: 214 | return 215 | await self._aircon.set_fanspeed(fanspeed) 216 | 217 | @property 218 | def swing_modes(self): 219 | """List of available swing modes.""" 220 | return [SWING_HORIZONTAL, SWING_OFF] 221 | 222 | @property 223 | def swing_mode(self): 224 | """Return the swing setting.""" 225 | return SWING_HORIZONTAL if self._aircon.get_h_louver_swing() else SWING_OFF 226 | 227 | async def async_set_swing_mode(self, swing_mode): 228 | """Set new target temperature.""" 229 | if swing_mode == SWING_HORIZONTAL: 230 | await self._aircon.set_h_louver_swing(True) 231 | else: 232 | await self._aircon.set_h_louver_swing(False) 233 | 234 | async def async_turn_on(self): 235 | """Turn device on.""" 236 | await self._aircon.set_power_on(True) 237 | 238 | async def async_turn_off(self): 239 | """Turn device off.""" 240 | await self._aircon.set_power_on(False) 241 | -------------------------------------------------------------------------------- /whirlpool/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "whirlpool", 3 | "name": "Whirlpool Sixth Sense", 4 | "config_flow": true, 5 | "documentation": "", 6 | "requirements": [ 7 | "whirlpool-sixth-sense==0.14.2" 8 | ], 9 | "codeowners": [ 10 | "@abmantis" 11 | ] 12 | } --------------------------------------------------------------------------------