2020-11-20 18:39:45 +00:00
|
|
|
# SPDX-FileCopyrightText: 2020 Andrius Štikonas <andrius@stikonas.eu>
|
|
|
|
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
|
|
|
|
import asyncio
|
2020-11-21 10:48:09 +00:00
|
|
|
import datetime
|
2020-11-20 18:39:45 +00:00
|
|
|
import json
|
2020-11-20 19:14:44 +00:00
|
|
|
import logging
|
2020-11-20 18:39:45 +00:00
|
|
|
|
2020-11-21 01:37:39 +00:00
|
|
|
from enums import ScheduleFormat
|
2020-11-20 22:45:37 +00:00
|
|
|
from system import System
|
2020-11-21 10:48:09 +00:00
|
|
|
from holiday import Holiday
|
2020-11-22 00:59:22 +00:00
|
|
|
from neostat import NeoStat
|
2020-11-20 22:45:37 +00:00
|
|
|
|
2020-11-21 01:37:39 +00:00
|
|
|
|
2020-11-20 18:39:45 +00:00
|
|
|
class NeoHub:
|
|
|
|
def __init__(self):
|
2020-11-20 19:14:44 +00:00
|
|
|
self._logger = logging.getLogger('neohub')
|
2020-11-20 18:39:45 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
async def connect(self, host='Neo-Hub', port='4242'):
|
2020-11-20 21:41:06 +00:00
|
|
|
self._host = host
|
|
|
|
self._port = port
|
2020-11-20 18:39:45 +00:00
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
async def _send(self, message, expected_reply=None):
|
2020-11-20 21:41:06 +00:00
|
|
|
reader, writer = await asyncio.open_connection(self._host, self._port)
|
2020-11-20 18:39:45 +00:00
|
|
|
encoded_message = bytearray(json.dumps(message) + "\0\r", "utf-8")
|
2020-11-20 19:38:02 +00:00
|
|
|
self._logger.debug(f"Sending message: {encoded_message}")
|
2020-11-20 21:41:06 +00:00
|
|
|
writer.write(encoded_message)
|
|
|
|
await writer.drain()
|
2020-11-20 18:39:45 +00:00
|
|
|
|
2020-11-20 21:59:04 +00:00
|
|
|
data = await reader.readuntil(b'\0')
|
|
|
|
data = data.strip(b'\0')
|
2020-11-20 21:41:06 +00:00
|
|
|
json_string = data.decode('utf-8')
|
|
|
|
self._logger.debug(f"Received message: {json_string}")
|
2020-11-20 21:59:04 +00:00
|
|
|
|
2020-11-20 21:41:06 +00:00
|
|
|
writer.close()
|
|
|
|
await writer.wait_closed()
|
2020-11-20 21:59:04 +00:00
|
|
|
|
2020-11-20 23:26:45 +00:00
|
|
|
try:
|
|
|
|
reply = json.loads(json_string)
|
2020-11-22 10:23:27 +00:00
|
|
|
except json.decoder.JSONDecodeError as e:
|
2020-11-20 23:26:45 +00:00
|
|
|
if expected_reply is None:
|
|
|
|
raise(e)
|
|
|
|
else:
|
|
|
|
return False
|
2020-11-20 23:13:16 +00:00
|
|
|
|
|
|
|
if expected_reply is None:
|
|
|
|
return reply
|
|
|
|
else:
|
|
|
|
if reply == expected_reply:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
self._logger.error(f"Unexpected reply: {reply}")
|
|
|
|
return False
|
2020-11-20 18:39:45 +00:00
|
|
|
|
|
|
|
async def firmware(self):
|
2020-11-20 19:38:02 +00:00
|
|
|
"""
|
2020-11-20 19:14:44 +00:00
|
|
|
NeoHub firmware version
|
2020-11-20 19:38:02 +00:00
|
|
|
"""
|
2020-11-20 23:13:16 +00:00
|
|
|
|
2020-11-20 18:39:45 +00:00
|
|
|
message = {"FIRMWARE": 0}
|
2020-11-20 21:41:06 +00:00
|
|
|
|
2020-11-20 19:38:02 +00:00
|
|
|
result = await self._send(message)
|
2020-11-20 19:14:44 +00:00
|
|
|
firmware_version = int(result['firmware version'])
|
|
|
|
return firmware_version
|
2020-11-20 19:38:02 +00:00
|
|
|
|
2020-11-20 22:45:37 +00:00
|
|
|
async def get_system(self):
|
|
|
|
"""
|
|
|
|
Get system wide variables
|
|
|
|
|
|
|
|
Returns System object
|
|
|
|
"""
|
|
|
|
message = {"GET_SYSTEM": 0}
|
|
|
|
|
|
|
|
data = await self._send(message)
|
|
|
|
system_data = System(data)
|
|
|
|
return system_data
|
|
|
|
|
2020-11-20 19:38:02 +00:00
|
|
|
async def reset(self):
|
|
|
|
"""
|
|
|
|
Reboot neohub
|
2020-11-20 21:41:06 +00:00
|
|
|
|
|
|
|
Returns True if Restart is initiated
|
2020-11-20 19:38:02 +00:00
|
|
|
"""
|
2020-11-20 23:13:16 +00:00
|
|
|
|
2020-11-20 19:38:02 +00:00
|
|
|
message = {"RESET": 0}
|
2020-11-20 23:13:16 +00:00
|
|
|
reply = {"Restarting": 1}
|
2020-11-20 21:41:06 +00:00
|
|
|
|
|
|
|
firmware_version = await self.firmware()
|
|
|
|
result = ""
|
|
|
|
if firmware_version >= 2027:
|
2020-11-20 23:13:16 +00:00
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-20 19:38:02 +00:00
|
|
|
else:
|
|
|
|
return False
|
2020-11-20 21:41:06 +00:00
|
|
|
|
2020-11-20 22:45:37 +00:00
|
|
|
async def set_channel(self, channel: int):
|
2020-11-20 21:41:06 +00:00
|
|
|
"""
|
2020-11-20 22:45:37 +00:00
|
|
|
Set ZigBee channel.
|
|
|
|
|
|
|
|
Only channels 11, 14, 15, 19, 20, 24, 25 are allowed.
|
2020-11-20 21:41:06 +00:00
|
|
|
"""
|
|
|
|
|
2020-11-20 22:45:37 +00:00
|
|
|
message = {"SET_CHANNEL": channel}
|
2020-11-20 23:17:40 +00:00
|
|
|
reply = {"result": "Trying to change channel"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
|
|
|
|
2020-11-21 01:49:26 +00:00
|
|
|
async def set_temp_format(self, temp_format: str):
|
2020-11-20 23:17:40 +00:00
|
|
|
"""
|
|
|
|
Set temperature format to C or F
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"SET_TEMP_FORMAT": temp_format}
|
|
|
|
reply = {"result": f"Temperature format set to {temp_format}"}
|
2020-11-20 22:45:37 +00:00
|
|
|
|
2020-11-20 23:13:16 +00:00
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-20 23:39:20 +00:00
|
|
|
|
2020-11-21 01:37:39 +00:00
|
|
|
async def set_format(self, format: ScheduleFormat):
|
2020-11-20 23:39:20 +00:00
|
|
|
"""
|
2020-11-21 01:37:39 +00:00
|
|
|
Sets schedule format
|
2020-11-20 23:39:20 +00:00
|
|
|
|
2020-11-21 01:37:39 +00:00
|
|
|
Format is specified using ScheduleFormat enum:
|
2020-11-20 23:39:20 +00:00
|
|
|
"""
|
|
|
|
|
2020-11-21 01:56:01 +00:00
|
|
|
message = {"SET_FORMAT": format}
|
2020-11-20 23:39:20 +00:00
|
|
|
reply = {"result": "Format was set"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-21 01:49:26 +00:00
|
|
|
|
|
|
|
async def set_away(self, state: bool):
|
|
|
|
"""
|
|
|
|
Enables away mode for all devices.
|
|
|
|
|
|
|
|
Puts thermostats into frost mode and timeclocks are set to off.
|
2020-11-22 10:23:27 +00:00
|
|
|
Instead of this function it is recommended to use frost on/off commands
|
2020-11-21 16:52:04 +00:00
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
List of affected devices can be restricted using GLOBAL_DEV_LIST command
|
2020-11-21 01:49:26 +00:00
|
|
|
"""
|
|
|
|
|
2020-11-21 01:56:01 +00:00
|
|
|
message = {"AWAY_ON" if state else "AWAY_OFF": 0}
|
2020-11-21 01:49:26 +00:00
|
|
|
reply = {"result": "away on" if state else "away off"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-21 01:56:01 +00:00
|
|
|
|
2020-11-21 10:48:09 +00:00
|
|
|
async def holiday(self, start: datetime.datetime, end: datetime.datetime):
|
|
|
|
"""
|
|
|
|
Sets holiday mode.
|
|
|
|
|
|
|
|
start: beginning of holiday
|
|
|
|
end: end of holiday
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"HOLIDAY": [start.strftime("%H%M%S%d%m%Y"), end.strftime("%H%M%S%d%m%Y")]}
|
|
|
|
|
|
|
|
result = await self._send(message)
|
|
|
|
return result
|
|
|
|
|
2020-11-21 01:56:01 +00:00
|
|
|
async def get_holiday(self):
|
|
|
|
"""
|
|
|
|
Get list of holidays
|
2020-11-21 10:48:09 +00:00
|
|
|
|
|
|
|
Returns Holiday object
|
2020-11-21 01:56:01 +00:00
|
|
|
"""
|
|
|
|
message = {"GET_HOLIDAY": 0}
|
|
|
|
|
|
|
|
result = await self._send(message)
|
2020-11-21 10:48:09 +00:00
|
|
|
return Holiday(result)
|
|
|
|
|
|
|
|
async def cancel_holiday(self):
|
|
|
|
"""
|
|
|
|
Cancels holidays and returns to normal schedule
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"CANCEL_HOLIDAY": 0}
|
|
|
|
reply = {"result": "holiday cancelled"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
2020-11-21 01:56:01 +00:00
|
|
|
return result
|
2020-11-21 11:44:40 +00:00
|
|
|
|
|
|
|
async def get_zones(self):
|
|
|
|
"""
|
2020-11-22 00:59:22 +00:00
|
|
|
Get list of all thermostats
|
2020-11-21 11:44:40 +00:00
|
|
|
|
2020-11-22 00:59:22 +00:00
|
|
|
Returns a list of NeoStat objects
|
2020-11-21 11:44:40 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"GET_ZONES": 0}
|
|
|
|
|
2020-11-22 00:59:22 +00:00
|
|
|
zones = await self._send(message)
|
|
|
|
result = []
|
2020-11-22 10:23:27 +00:00
|
|
|
for name, zone_id in zones.items():
|
|
|
|
result.append(NeoStat(self, name, zone_id))
|
2020-11-22 00:59:22 +00:00
|
|
|
|
2020-11-21 11:44:40 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
async def get_devices(self):
|
|
|
|
"""
|
|
|
|
Returns list of devices
|
|
|
|
|
|
|
|
{"result": ["device1"]}
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"GET_DEVICES": 0}
|
|
|
|
|
|
|
|
result = await self._send(message)
|
|
|
|
return result
|
2020-11-21 11:55:21 +00:00
|
|
|
|
|
|
|
async def get_device_list(self, zone: str):
|
|
|
|
"""
|
|
|
|
Returns list of devices associated with zone
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"GET_DEVICE_LIST": zone}
|
|
|
|
|
|
|
|
result = await self._send(message)
|
|
|
|
if 'error' in result:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return result[zone]
|
|
|
|
|
|
|
|
async def devices_sn(self):
|
|
|
|
"""
|
|
|
|
Returns serial numbers of attached devices
|
|
|
|
|
|
|
|
{'name': [id, 'serial', 1], ...}
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"DEVICES_SN": 0}
|
|
|
|
|
|
|
|
result = await self._send(message)
|
|
|
|
return result
|
2020-11-21 16:52:04 +00:00
|
|
|
|
|
|
|
async def set_ntp(self, state: bool):
|
|
|
|
"""
|
|
|
|
Enables NTP client on Neohub
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"NTP_ON" if state else "NTP_OFF": 0}
|
|
|
|
reply = {"result": "ntp client started" if state else "ntp client stopped"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-21 17:11:49 +00:00
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
async def set_date(self, date=None):
|
2020-11-21 17:11:49 +00:00
|
|
|
"""
|
|
|
|
Sets current date
|
|
|
|
|
|
|
|
By default, set to current date. Can be optionally passed datetime.datetime object
|
|
|
|
"""
|
|
|
|
|
|
|
|
if date is None:
|
|
|
|
date = datetime.datetime.today()
|
|
|
|
|
|
|
|
message = {"SET_DATE": [date.year, date.month, date.day]}
|
|
|
|
reply = {"result": "Date is set"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
async def set_time(self, time=None):
|
2020-11-21 17:11:49 +00:00
|
|
|
"""
|
|
|
|
Sets current time
|
|
|
|
|
|
|
|
By default, set to current time. Can be optionally passed datetime.datetime object
|
|
|
|
"""
|
|
|
|
|
|
|
|
if time is None:
|
|
|
|
time = datetime.datetime.now()
|
|
|
|
|
|
|
|
message = {"SET_TIME": [time.hour, time.minute]}
|
|
|
|
reply = {"result": "time set"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
async def set_datetime(self, date_time=None):
|
2020-11-21 17:11:49 +00:00
|
|
|
"""
|
|
|
|
Convenience method to set both date and time
|
|
|
|
"""
|
|
|
|
|
|
|
|
result = await self.set_date(date_time)
|
|
|
|
if result:
|
|
|
|
result = await self.set_time(date_time)
|
|
|
|
return result
|
2020-11-21 18:08:25 +00:00
|
|
|
|
|
|
|
async def manual_dst(self, state: bool):
|
|
|
|
"""
|
|
|
|
Manually enables/disables daylight saving time
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"MANUAL_DST": int(state)}
|
|
|
|
reply = {"result": "Updated time"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
|
|
|
|
2020-11-22 10:23:27 +00:00
|
|
|
async def set_dst(self, state: bool, region=None):
|
2020-11-21 18:08:25 +00:00
|
|
|
"""
|
|
|
|
Enables/disables automatic DST handling.
|
|
|
|
|
|
|
|
By default it uses UK dates for turning DST on/off.
|
|
|
|
Available options for region are UK, EU, NZ.
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"DST_ON" if state else "DST_OFF": 0 if region is None else region}
|
|
|
|
reply = {"result": "dst on" if state else "dst off"}
|
|
|
|
|
|
|
|
valid_timezones = ["UK", "EU", "NZ"]
|
|
|
|
if region not in valid_timezones:
|
|
|
|
return False
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|
2020-11-22 00:37:53 +00:00
|
|
|
|
|
|
|
async def identify(self):
|
|
|
|
"""
|
|
|
|
Flashes red LED light
|
|
|
|
"""
|
|
|
|
|
|
|
|
message = {"IDENTIFY": 0}
|
|
|
|
reply = {"result": "flashing led"}
|
|
|
|
|
|
|
|
result = await self._send(message, reply)
|
|
|
|
return result
|