From 358db23c9e2d18a37738d8b0038b8f1f23393730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 9 Oct 2018 18:56:33 +0200 Subject: [PATCH 1/5] initial version of millheater --- .coveragerc | 1 + homeassistant/components/climate/mill.py | 492 +++++++++++++++++++++++ 2 files changed, 493 insertions(+) create mode 100644 homeassistant/components/climate/mill.py diff --git a/.coveragerc b/.coveragerc index 801932b19fba6c..f6ec034f9a6cf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -437,6 +437,7 @@ omit = homeassistant/components/climate/homematic.py homeassistant/components/climate/honeywell.py homeassistant/components/climate/knx.py + homeassistant/components/climate/mill.py homeassistant/components/climate/oem.py homeassistant/components/climate/opentherm_gw.py homeassistant/components/climate/proliphix.py diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py new file mode 100644 index 00000000000000..8eb2aa83bca21d --- /dev/null +++ b/homeassistant/components/climate/mill.py @@ -0,0 +1,492 @@ +""" +Support for mill wifi-enabled home heaters. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.mill/ +""" + +import logging + +import voluptuous as vol +from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_EMAIL, CONF_PASSWORD, + STATE_ON, STATE_OFF, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import dt as dt_util + +REQUIREMENTS = [] + +_LOGGER = logging.getLogger(__name__) + +MAX_TEMP = 35 +MIN_TEMP = 5 +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | + SUPPORT_FAN_MODE | SUPPORT_ON_OFF) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the Mill heater.""" + mill_data_connection = Mill(config[CONF_EMAIL], + config[CONF_PASSWORD], + websession=async_get_clientsession(hass)) + if not await mill_data_connection.connect(): + _LOGGER.error("Failed to connect to Mill") + return + + await mill_data_connection.update_heaters() + + dev = [] + for heater in mill_data_connection.heaters.values(): + dev.append(MilHeater(heater, mill_data_connection)) + async_add_entities(dev) + + +class MilHeater(ClimateDevice): + """Representation of a Mill Thermostat device.""" + + def __init__(self, heater, mill_data_connection): + """Initialize the thermostat.""" + self._heater = heater + self._conn = mill_data_connection + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def state(self): + """Return the current state.""" + return STATE_ON if self._heater.power_status == 1 else STATE_OFF + + @property + def unique_id(self): + """Return a unique ID.""" + return self._heater.device_id + + @property + def name(self): + """Return the name of the entity.""" + return self._heater.name + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._heater.set_temp + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._heater.current_temp + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return STATE_ON if self._heater.fan_status == 1 else STATE_OFF + + @property + def fan_list(self): + """List of available fan modes.""" + return [STATE_ON, STATE_OFF] + + @property + def is_on(self): + """Return true if heater is on.""" + return True if self._heater.power_status == 1 else False + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMP + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + temperature = int(temperature) + await self._conn.set_heater_temp(self._heater.device_id, + temperature) + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + fan_status = 1 if fan_mode == STATE_ON else 0 + await self._conn.heater_control(self._heater.device_id, + fan_status=fan_status) + + async def async_turn_on(self): + """Turn Mill unit on.""" + await self._conn.heater_control(self._heater.device_id, + power_status=1) + + async def async_turn_off(self): + """Turn Mill unit off.""" + await self._conn.heater_control(self._heater.device_id, + power_status=0) + + async def async_update(self): + """Retrieve latest state.""" + self._heater = await self._conn.update_device(self._heater.device_id) + + + + +"""Library to handle connection with mill.""" +import asyncio +import datetime as dt +import json +import logging + +import aiohttp +import async_timeout +import hashlib +import random +import string +import time + +API_ENDPOINT_1 = 'https://eurouter.ablecloud.cn:9005/zc-account/v1' +API_ENDPOINT_2 = 'http://eurouter.ablecloud.cn:5000/millService/v1' +DEFAULT_TIMEOUT = 10 +MIN_TIME_BETWEEN_UPDATES = dt_util.dt.timedelta(seconds=10) + + +_LOGGER = logging.getLogger(__name__) + + +class Mill: + """Class to comunicate with the Mill api.""" + + def __init__(self, username, password, + timeout=DEFAULT_TIMEOUT, + websession=None): + """Initialize the Mill connection.""" + if websession is None: + async def _create_session(): + return aiohttp.ClientSession() + loop = asyncio.get_event_loop() + self.websession = loop.run_until_complete(_create_session()) + else: + self.websession = websession + self._timeout = timeout + self._username = username + self._password = password + self._user_id = None + self._token = None + self.rooms = {} + self.heaters = {} + self._throttle_time = None + + async def connect(self): + """Connect to Mill.""" + url = API_ENDPOINT_1 + '/login' + headers = { + "Content-Type": "application/x-zc-object", + "Connection": "Keep-Alive", + "X-Zc-Major-Domain": "seanywell", + "X-Zc-Msg-Name": "millService", + "X-Zc-Sub-Domain": "milltype", + "X-Zc-Seq-Id": "1", + "X-Zc-Version": "1", + } + payload = {"account": self._username, + "password": self._password} + try: + with async_timeout.timeout(self._timeout): + resp = await self.websession.post(url, + data=json.dumps(payload), + headers=headers) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + _LOGGER.error("Error connecting to Mill: %s", err) + return False + + result = await resp.text() + _LOGGER.debug(result) + if '"errorCode":3504' in result: + _LOGGER.error('Wrong password') + return False + + if '"errorCode":3501' in result: + _LOGGER.error('Account does not exist') + return False + + data = json.loads(result) + self._user_id = data.get('userId') + self._token = data.get('token') + return True + + def sync_connect(self): + """Close the Mill connection.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.connect()) + loop.run_until_complete(task) + + async def close_connection(self): + """Close the Mill connection.""" + await self.websession.close() + + def sync_close_connection(self): + """Close the Mill connection.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.close_connection()) + loop.run_until_complete(task) + + async def request(self, command, payload, retry=2): + """Request data.""" + + if self._token is None: + _LOGGER.error("No token") + return + + _LOGGER.debug(payload) + + nonce = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16)) + url = API_ENDPOINT_2 + command + timestamp = int(time.time()) + signature = hashlib.sha1(str('300' + + str(timestamp) + + nonce + + self._token).encode("utf-8")).hexdigest() + + headers = { + "Content-Type": "application/x-zc-object", + "Connection": "Keep-Alive", + "X-Zc-Major-Domain": "seanywell", + "X-Zc-Msg-Name": "millService", + "X-Zc-Sub-Domain": "milltype", + "X-Zc-Seq-Id": "1", + "X-Zc-Version": "1", + "X-Zc-Timestamp": str(timestamp), + "X-Zc-Timeout": "300", + "X-Zc-Nonce": nonce, + "X-Zc-User-Id": str(self._user_id), + "X-Zc-User-Signature": signature, + "X-Zc-Content-Length": str(len(payload)), + } + try: + with async_timeout.timeout(self._timeout): + resp = await self.websession.post(url, + data=json.dumps(payload), + headers=headers) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + _LOGGER.error("Error sending command to Mill: %s", err) + return None + + result = await resp.text() + + _LOGGER.debug(result) + + if not result or result == '{"errorCode":0}': + return None + + if 'access token expire' in result: + if retry < 1: + return None + if not await self.connect(): + return None + return await self.request(command, payload, retry-1) + + if 'errorCode' in result: + _LOGGER.error("Failed to send request, %s", result) + return None + + data = json.loads(result) + return data + + async def get_home_list(self): + """Request data.""" + resp = await self.request("/selectHomeList", "{}") + if resp is None: + return None + homes = resp.get('homeList') + return homes + + async def update_rooms(self): + """Request data.""" + homes = await self.get_home_list() + if homes is None: + return None + for home in homes: + payload = {"homeId": home.get("homeId"), "timeZoneNum": "+01:00"} + data = await self.request("/selectRoombyHome", payload) + rooms = data.get('roomInfo', []) + if not rooms: + continue + for _room in rooms: + _id = _room.get('roomId') + room = self.rooms.get(_id, Room()) + room.room_id = _id + room.comfort_temp = _room.get("comfortTemp") + room.away_temp = _room.get("awayTemp") + room.sleep_temp = _room.get("sleepTemp") + room.name = _room.get("roomName") + room.current_mode = _room.get("currentMode") + room.heat_status = _room.get("heatStatus") + + self.rooms[_id] = room + + def sync_update_rooms(self): + """Request data.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.update_rooms()) + loop.run_until_complete(task) + + async def set_room_temperatures(self, room_id, sleep_temp=None, + comfort_temp=None, away_temp=None): + """Set room temps.""" + room = self.rooms.get(room_id) + if room is None: + _LOGGER.error("No such device") + return + if sleep_temp is None: + sleep_temp = room.sleep_temp + if away_temp is None: + away_temp = room.away_temp + if comfort_temp is None: + comfort_temp = room.comfort_temp + payload = {"roomId": room_id, + "sleepTemp": sleep_temp, + "comfortTemp": comfort_temp, + "awayTemp": away_temp, + "homeType": 0} + await self.request("/changeRoomModeTempInfo", payload) + + def sync_set_room_temperatures(self, room_id, sleep_temp=None, + comfort_temp=None, away_temp=None): + """Set heater temps.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.set_room_temperatures(room_id, + sleep_temp, + comfort_temp, + away_temp)) + loop.run_until_complete(task) + + async def update_heaters(self): + """Request data.""" + homes = await self.get_home_list() + if homes is None: + return None + for home in homes: + payload = {"homeId": home.get("homeId")} + data = await self.request("/getIndependentDevices", payload) + heaters = data.get('deviceInfo', []) + if not heaters: + continue + for _heater in heaters: + _id = _heater.get('deviceId') + heater = self.heaters.get(_id, Heater()) + heater.device_id = _id + heater.current_temp = _heater.get('currentTemp') + heater.name = _heater.get('deviceName') + heater.fan_status = _heater.get('fanStatus') + heater.set_temp = _heater.get('holidayTemp') + heater.power_status = _heater.get('powerStatus') + + self.heaters[_id] = heater + + def sync_update_heaters(self): + """Request data.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.update_heaters()) + loop.run_until_complete(task) + + async def throttle_update_heaters(self): + if (self._throttle_time is not None + and dt.datetime.now() - self._throttle_time < MIN_TIME_BETWEEN_UPDATES): + return + self._throttle_time = dt.datetime.now() + await self.update_heaters() + + async def update_device(self, device_id): + """Find the current data from self.data.""" + await self.throttle_update_heaters() + return self.heaters.get(device_id) + + async def heater_control(self, device_id, fan_status=None, + power_status=None): + """Set heater temps.""" + heater = self.heaters.get(device_id) + if heater is None: + _LOGGER.error("No such device") + return + if fan_status is None: + fan_status = heater.fan_status + if power_status is None: + power_status = heater.power_status + operation = 0 if fan_status == heater.fan_status else 4 + payload = {"subDomain": 5332, + "deviceId": device_id, + "testStatus": 1, + "operation": operation, + "status": power_status, + "windStatus": fan_status, + "holdTemp": heater.set_temp, + "tempType": 0, + "powerLevel": 0} + await self.request("/deviceControl", payload) + + def sync_heater_control(self, device_id, fan_status=None, + power_status=None): + """Set heater temps.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.heater_control(device_id, + fan_status, + power_status)) + loop.run_until_complete(task) + + async def set_heater_temp(self, device_id, set_temp): + """Set heater temp.""" + payload = {"homeType": 0, + "timeZoneNum": "+02:00", + "deviceId": device_id, + "value": set_temp, + "key": "holidayTemp"} + await self.request("/changeDeviceInfo", payload) + + def sync_set_heater_temp(self, device_id, set_temp): + """Set heater temps.""" + loop = asyncio.get_event_loop() + task = loop.create_task(self.set_heater_temp(device_id, set_temp)) + loop.run_until_complete(task) + + +class Room: + room_id = None + comfort_temp = None + away_temp = None + sleep_temp = None + name = None + is_offline = None + heat_status = None + + +class Heater: + device_id = None + current_temp = None + name = None + set_temp = None + fan_status = None + power_status = None From d7c2952922a81cff119d237a4a886e319669d080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 9 Oct 2018 19:49:21 +0200 Subject: [PATCH 2/5] Remove unused imports --- homeassistant/components/climate/mill.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index 8eb2aa83bca21d..e00e6c6edd5421 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -1,5 +1,6 @@ """ Support for mill wifi-enabled home heaters. + For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.mill/ """ @@ -11,7 +12,7 @@ ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_EMAIL, CONF_PASSWORD, + ATTR_TEMPERATURE, CONF_EMAIL, CONF_PASSWORD, STATE_ON, STATE_OFF, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -256,7 +257,6 @@ def sync_close_connection(self): async def request(self, command, payload, retry=2): """Request data.""" - if self._token is None: _LOGGER.error("No token") return @@ -414,6 +414,7 @@ def sync_update_heaters(self): loop.run_until_complete(task) async def throttle_update_heaters(self): + """Throttle update device.""" if (self._throttle_time is not None and dt.datetime.now() - self._throttle_time < MIN_TIME_BETWEEN_UPDATES): return @@ -421,7 +422,7 @@ async def throttle_update_heaters(self): await self.update_heaters() async def update_device(self, device_id): - """Find the current data from self.data.""" + """Update device.""" await self.throttle_update_heaters() return self.heaters.get(device_id) @@ -449,7 +450,7 @@ async def heater_control(self, device_id, fan_status=None, await self.request("/deviceControl", payload) def sync_heater_control(self, device_id, fan_status=None, - power_status=None): + power_status=None): """Set heater temps.""" loop = asyncio.get_event_loop() task = loop.create_task(self.heater_control(device_id, @@ -474,6 +475,7 @@ def sync_set_heater_temp(self, device_id, set_temp): class Room: + """Representation of room.""" room_id = None comfort_temp = None away_temp = None @@ -484,6 +486,7 @@ class Room: class Heater: + """Representation of heater.""" device_id = None current_temp = None name = None From 137c4ee0436c9f207ea455c8f0bb259c0444890e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 9 Oct 2018 21:19:54 +0200 Subject: [PATCH 3/5] Add some comments --- homeassistant/components/climate/mill.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index e00e6c6edd5421..ab06047f91b233 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -64,6 +64,11 @@ def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS + @property + def available(self): + """Return True if entity is available.""" + return self._heater.device_id == 0 + @property def state(self): """Return the current state.""" @@ -157,6 +162,8 @@ async def async_update(self): """Library to handle connection with mill.""" +# Based on https://pastebin.com/53Nk0wJA and Postman capturing from the app +# All requests are send unencrypted from the :( import asyncio import datetime as dt import json @@ -400,6 +407,7 @@ async def update_heaters(self): heater = self.heaters.get(_id, Heater()) heater.device_id = _id heater.current_temp = _heater.get('currentTemp') + heater.device_status = _heater.get('deviceStatus') heater.name = _heater.get('deviceName') heater.fan_status = _heater.get('fanStatus') heater.set_temp = _heater.get('holidayTemp') @@ -476,6 +484,7 @@ def sync_set_heater_temp(self, device_id, set_temp): class Room: """Representation of room.""" + room_id = None comfort_temp = None away_temp = None @@ -487,6 +496,7 @@ class Room: class Heater: """Representation of heater.""" + device_id = None current_temp = None name = None From d9618cab9d563df625dcdd931c16ba04d2b51f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 10 Oct 2018 18:02:27 +0200 Subject: [PATCH 4/5] separate lib --- homeassistant/components/climate/mill.py | 359 +---------------------- requirements_all.txt | 3 + 2 files changed, 9 insertions(+), 353 deletions(-) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index ab06047f91b233..61f55e2ea16634 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -9,16 +9,15 @@ import voluptuous as vol from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF) + ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_EMAIL, CONF_PASSWORD, STATE_ON, STATE_OFF, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import dt as dt_util -REQUIREMENTS = [] +REQUIREMENTS = ['millheater==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -36,6 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Mill heater.""" + from mill import Mill mill_data_connection = Mill(config[CONF_EMAIL], config[CONF_PASSWORD], websession=async_get_clientsession(hass)) @@ -67,7 +67,7 @@ def supported_features(self): @property def available(self): """Return True if entity is available.""" - return self._heater.device_id == 0 + return self._heater.device_status == 0 # weird api choice @property def state(self): @@ -134,9 +134,8 @@ async def async_set_temperature(self, **kwargs): temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - temperature = int(temperature) await self._conn.set_heater_temp(self._heater.device_id, - temperature) + int(temperature)) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -157,349 +156,3 @@ async def async_turn_off(self): async def async_update(self): """Retrieve latest state.""" self._heater = await self._conn.update_device(self._heater.device_id) - - - - -"""Library to handle connection with mill.""" -# Based on https://pastebin.com/53Nk0wJA and Postman capturing from the app -# All requests are send unencrypted from the :( -import asyncio -import datetime as dt -import json -import logging - -import aiohttp -import async_timeout -import hashlib -import random -import string -import time - -API_ENDPOINT_1 = 'https://eurouter.ablecloud.cn:9005/zc-account/v1' -API_ENDPOINT_2 = 'http://eurouter.ablecloud.cn:5000/millService/v1' -DEFAULT_TIMEOUT = 10 -MIN_TIME_BETWEEN_UPDATES = dt_util.dt.timedelta(seconds=10) - - -_LOGGER = logging.getLogger(__name__) - - -class Mill: - """Class to comunicate with the Mill api.""" - - def __init__(self, username, password, - timeout=DEFAULT_TIMEOUT, - websession=None): - """Initialize the Mill connection.""" - if websession is None: - async def _create_session(): - return aiohttp.ClientSession() - loop = asyncio.get_event_loop() - self.websession = loop.run_until_complete(_create_session()) - else: - self.websession = websession - self._timeout = timeout - self._username = username - self._password = password - self._user_id = None - self._token = None - self.rooms = {} - self.heaters = {} - self._throttle_time = None - - async def connect(self): - """Connect to Mill.""" - url = API_ENDPOINT_1 + '/login' - headers = { - "Content-Type": "application/x-zc-object", - "Connection": "Keep-Alive", - "X-Zc-Major-Domain": "seanywell", - "X-Zc-Msg-Name": "millService", - "X-Zc-Sub-Domain": "milltype", - "X-Zc-Seq-Id": "1", - "X-Zc-Version": "1", - } - payload = {"account": self._username, - "password": self._password} - try: - with async_timeout.timeout(self._timeout): - resp = await self.websession.post(url, - data=json.dumps(payload), - headers=headers) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Error connecting to Mill: %s", err) - return False - - result = await resp.text() - _LOGGER.debug(result) - if '"errorCode":3504' in result: - _LOGGER.error('Wrong password') - return False - - if '"errorCode":3501' in result: - _LOGGER.error('Account does not exist') - return False - - data = json.loads(result) - self._user_id = data.get('userId') - self._token = data.get('token') - return True - - def sync_connect(self): - """Close the Mill connection.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.connect()) - loop.run_until_complete(task) - - async def close_connection(self): - """Close the Mill connection.""" - await self.websession.close() - - def sync_close_connection(self): - """Close the Mill connection.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.close_connection()) - loop.run_until_complete(task) - - async def request(self, command, payload, retry=2): - """Request data.""" - if self._token is None: - _LOGGER.error("No token") - return - - _LOGGER.debug(payload) - - nonce = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(16)) - url = API_ENDPOINT_2 + command - timestamp = int(time.time()) - signature = hashlib.sha1(str('300' - + str(timestamp) - + nonce - + self._token).encode("utf-8")).hexdigest() - - headers = { - "Content-Type": "application/x-zc-object", - "Connection": "Keep-Alive", - "X-Zc-Major-Domain": "seanywell", - "X-Zc-Msg-Name": "millService", - "X-Zc-Sub-Domain": "milltype", - "X-Zc-Seq-Id": "1", - "X-Zc-Version": "1", - "X-Zc-Timestamp": str(timestamp), - "X-Zc-Timeout": "300", - "X-Zc-Nonce": nonce, - "X-Zc-User-Id": str(self._user_id), - "X-Zc-User-Signature": signature, - "X-Zc-Content-Length": str(len(payload)), - } - try: - with async_timeout.timeout(self._timeout): - resp = await self.websession.post(url, - data=json.dumps(payload), - headers=headers) - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - _LOGGER.error("Error sending command to Mill: %s", err) - return None - - result = await resp.text() - - _LOGGER.debug(result) - - if not result or result == '{"errorCode":0}': - return None - - if 'access token expire' in result: - if retry < 1: - return None - if not await self.connect(): - return None - return await self.request(command, payload, retry-1) - - if 'errorCode' in result: - _LOGGER.error("Failed to send request, %s", result) - return None - - data = json.loads(result) - return data - - async def get_home_list(self): - """Request data.""" - resp = await self.request("/selectHomeList", "{}") - if resp is None: - return None - homes = resp.get('homeList') - return homes - - async def update_rooms(self): - """Request data.""" - homes = await self.get_home_list() - if homes is None: - return None - for home in homes: - payload = {"homeId": home.get("homeId"), "timeZoneNum": "+01:00"} - data = await self.request("/selectRoombyHome", payload) - rooms = data.get('roomInfo', []) - if not rooms: - continue - for _room in rooms: - _id = _room.get('roomId') - room = self.rooms.get(_id, Room()) - room.room_id = _id - room.comfort_temp = _room.get("comfortTemp") - room.away_temp = _room.get("awayTemp") - room.sleep_temp = _room.get("sleepTemp") - room.name = _room.get("roomName") - room.current_mode = _room.get("currentMode") - room.heat_status = _room.get("heatStatus") - - self.rooms[_id] = room - - def sync_update_rooms(self): - """Request data.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.update_rooms()) - loop.run_until_complete(task) - - async def set_room_temperatures(self, room_id, sleep_temp=None, - comfort_temp=None, away_temp=None): - """Set room temps.""" - room = self.rooms.get(room_id) - if room is None: - _LOGGER.error("No such device") - return - if sleep_temp is None: - sleep_temp = room.sleep_temp - if away_temp is None: - away_temp = room.away_temp - if comfort_temp is None: - comfort_temp = room.comfort_temp - payload = {"roomId": room_id, - "sleepTemp": sleep_temp, - "comfortTemp": comfort_temp, - "awayTemp": away_temp, - "homeType": 0} - await self.request("/changeRoomModeTempInfo", payload) - - def sync_set_room_temperatures(self, room_id, sleep_temp=None, - comfort_temp=None, away_temp=None): - """Set heater temps.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.set_room_temperatures(room_id, - sleep_temp, - comfort_temp, - away_temp)) - loop.run_until_complete(task) - - async def update_heaters(self): - """Request data.""" - homes = await self.get_home_list() - if homes is None: - return None - for home in homes: - payload = {"homeId": home.get("homeId")} - data = await self.request("/getIndependentDevices", payload) - heaters = data.get('deviceInfo', []) - if not heaters: - continue - for _heater in heaters: - _id = _heater.get('deviceId') - heater = self.heaters.get(_id, Heater()) - heater.device_id = _id - heater.current_temp = _heater.get('currentTemp') - heater.device_status = _heater.get('deviceStatus') - heater.name = _heater.get('deviceName') - heater.fan_status = _heater.get('fanStatus') - heater.set_temp = _heater.get('holidayTemp') - heater.power_status = _heater.get('powerStatus') - - self.heaters[_id] = heater - - def sync_update_heaters(self): - """Request data.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.update_heaters()) - loop.run_until_complete(task) - - async def throttle_update_heaters(self): - """Throttle update device.""" - if (self._throttle_time is not None - and dt.datetime.now() - self._throttle_time < MIN_TIME_BETWEEN_UPDATES): - return - self._throttle_time = dt.datetime.now() - await self.update_heaters() - - async def update_device(self, device_id): - """Update device.""" - await self.throttle_update_heaters() - return self.heaters.get(device_id) - - async def heater_control(self, device_id, fan_status=None, - power_status=None): - """Set heater temps.""" - heater = self.heaters.get(device_id) - if heater is None: - _LOGGER.error("No such device") - return - if fan_status is None: - fan_status = heater.fan_status - if power_status is None: - power_status = heater.power_status - operation = 0 if fan_status == heater.fan_status else 4 - payload = {"subDomain": 5332, - "deviceId": device_id, - "testStatus": 1, - "operation": operation, - "status": power_status, - "windStatus": fan_status, - "holdTemp": heater.set_temp, - "tempType": 0, - "powerLevel": 0} - await self.request("/deviceControl", payload) - - def sync_heater_control(self, device_id, fan_status=None, - power_status=None): - """Set heater temps.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.heater_control(device_id, - fan_status, - power_status)) - loop.run_until_complete(task) - - async def set_heater_temp(self, device_id, set_temp): - """Set heater temp.""" - payload = {"homeType": 0, - "timeZoneNum": "+02:00", - "deviceId": device_id, - "value": set_temp, - "key": "holidayTemp"} - await self.request("/changeDeviceInfo", payload) - - def sync_set_heater_temp(self, device_id, set_temp): - """Set heater temps.""" - loop = asyncio.get_event_loop() - task = loop.create_task(self.set_heater_temp(device_id, set_temp)) - loop.run_until_complete(task) - - -class Room: - """Representation of room.""" - - room_id = None - comfort_temp = None - away_temp = None - sleep_temp = None - name = None - is_offline = None - heat_status = None - - -class Heater: - """Representation of heater.""" - - device_id = None - current_temp = None - name = None - set_temp = None - fan_status = None - power_status = None diff --git a/requirements_all.txt b/requirements_all.txt index 1643897927d5b5..3fbb35cee84b15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -597,6 +597,9 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.4.0 +# homeassistant.components.climate.mill +millheater==0.1.1 + # homeassistant.components.sensor.mitemp_bt mitemp_bt==0.0.1 From 96aeaacc45fb8dcf3f556cff70f1507a029f7a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 14 Oct 2018 19:14:54 +0200 Subject: [PATCH 5/5] fix review comments --- homeassistant/components/climate/mill.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index 61f55e2ea16634..5185cf115e22ba 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -12,7 +12,7 @@ ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_EMAIL, CONF_PASSWORD, + ATTR_TEMPERATURE, CONF_PASSWORD, CONF_USERNAME, STATE_ON, STATE_OFF, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -27,7 +27,7 @@ SUPPORT_FAN_MODE | SUPPORT_ON_OFF) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, }) @@ -36,7 +36,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Mill heater.""" from mill import Mill - mill_data_connection = Mill(config[CONF_EMAIL], + mill_data_connection = Mill(config[CONF_USERNAME], config[CONF_PASSWORD], websession=async_get_clientsession(hass)) if not await mill_data_connection.connect(): @@ -47,11 +47,11 @@ async def async_setup_platform(hass, config, async_add_entities, dev = [] for heater in mill_data_connection.heaters.values(): - dev.append(MilHeater(heater, mill_data_connection)) + dev.append(MillHeater(heater, mill_data_connection)) async_add_entities(dev) -class MilHeater(ClimateDevice): +class MillHeater(ClimateDevice): """Representation of a Mill Thermostat device.""" def __init__(self, heater, mill_data_connection): @@ -69,11 +69,6 @@ def available(self): """Return True if entity is available.""" return self._heater.device_status == 0 # weird api choice - @property - def state(self): - """Return the current state.""" - return STATE_ON if self._heater.power_status == 1 else STATE_OFF - @property def unique_id(self): """Return a unique ID.""" @@ -117,7 +112,7 @@ def fan_list(self): @property def is_on(self): """Return true if heater is on.""" - return True if self._heater.power_status == 1 else False + return self._heater.power_status == 1 @property def min_temp(self):