V2.0
This commit is contained in:
parent
2305ac05bd
commit
91c95149a3
|
@ -34,7 +34,7 @@ class NeoHub:
|
|||
def __init__(self, host='Neo-Hub', port=4242, request_timeout=60, request_attempts=1, token=None):
|
||||
self._logger = logging.getLogger('neohub')
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._port = int(port)
|
||||
self._request_timeout = request_timeout
|
||||
self._request_attempts = request_attempts
|
||||
self._token = token
|
||||
|
@ -67,7 +67,7 @@ class NeoHub:
|
|||
async def _send(self, message, expected_reply=None):
|
||||
last_exception = None
|
||||
writer = None
|
||||
for attempt in range(1, self._request_attempts+1):
|
||||
for attempt in range(1, self._request_attempts + 1):
|
||||
try:
|
||||
if self._token is not None:
|
||||
# Websocket connection on port 4243, introduced in v3 of the API on 12/12/21
|
||||
|
@ -98,7 +98,7 @@ class NeoHub:
|
|||
self._logger.debug("Using legacy connection")
|
||||
reader, writer = await asyncio.open_connection(self._host, self._port)
|
||||
data = await asyncio.wait_for(
|
||||
self._send_message(reader, writer, message), timeout=self._request_timeout)
|
||||
self._send_message(reader, writer, message), timeout=self._request_timeout)
|
||||
json_string = data.decode('utf-8')
|
||||
|
||||
self._logger.debug(f"Received message: {json_string}")
|
||||
|
@ -133,6 +133,15 @@ class NeoHub:
|
|||
raise last_exception
|
||||
return False
|
||||
|
||||
def _devices_to_device_ids(self, devices: [NeoStat]):
|
||||
"""
|
||||
Returns the list of device ids
|
||||
"""
|
||||
try:
|
||||
return [x.device_id for x in devices]
|
||||
except (TypeError, AttributeError):
|
||||
raise NeoHubUsageError('devices must be a list of NeoStat objects')
|
||||
|
||||
def _devices_to_names(self, devices: [NeoStat]):
|
||||
"""
|
||||
Returns the list of device names
|
||||
|
@ -290,9 +299,9 @@ class NeoHub:
|
|||
|
||||
result = await self._send(message)
|
||||
result.start = datetime.datetime.strptime(
|
||||
result.start.strip(), "%a %b %d %H:%M:%S %Y") if result.start else None
|
||||
result.start.strip(), "%a %b %d %H:%M:%S %Y") if result.start else None
|
||||
result.end = datetime.datetime.strptime(
|
||||
result.end.strip(), "%a %b %d %H:%M:%S %Y") if result.end else None
|
||||
result.end.strip(), "%a %b %d %H:%M:%S %Y") if result.end else None
|
||||
return result
|
||||
|
||||
async def cancel_holiday(self):
|
||||
|
@ -435,44 +444,9 @@ class NeoHub:
|
|||
"""
|
||||
|
||||
message = {"GET_LIVE_DATA": 0}
|
||||
hub_data = await self._send(message)
|
||||
live_data = await self._send(message)
|
||||
|
||||
# We need the engineers data to get things like Device Type, Sensor Mode etc.
|
||||
get_eng_data_msg = {"GET_ENGINEERS": 0}
|
||||
eng_hub_data = await self._send(get_eng_data_msg)
|
||||
|
||||
devices = hub_data.devices
|
||||
delattr(hub_data, "devices")
|
||||
|
||||
# Combine the live data and engineers data to produce a full dataset to work from.
|
||||
# Start by working through each device in the devices
|
||||
for device in devices:
|
||||
|
||||
# Find matching device ID in Engineers data
|
||||
for key, value in eng_hub_data.__dict__.items():
|
||||
if device.DEVICE_ID == value.DEVICE_ID:
|
||||
|
||||
# Now add the engineers data dictionary entries to the existing device dictionary.
|
||||
for x in value.__dict__.items():
|
||||
setattr(device, x[0], x[1])
|
||||
|
||||
thermostat_list = list(filter(lambda device: hasattr(device, 'THERMOSTAT') and device.THERMOSTAT, devices))
|
||||
timeclock_list = list(filter(lambda device: hasattr(device, 'TIMECLOCK') and device.TIMECLOCK, devices))
|
||||
|
||||
thermostats = []
|
||||
timeclocks = []
|
||||
|
||||
for thermostat in thermostat_list:
|
||||
thermostats.append(NeoStat(self, thermostat))
|
||||
|
||||
for timeclock in timeclock_list:
|
||||
timeclocks.append(NeoStat(self, timeclock))
|
||||
|
||||
devices = {}
|
||||
devices['thermostats'] = thermostats
|
||||
devices['timeclocks'] = timeclocks
|
||||
|
||||
return hub_data, devices
|
||||
return live_data
|
||||
|
||||
async def permit_join(self, name, timeout_s: int = 120):
|
||||
"""
|
||||
|
@ -615,6 +589,32 @@ class NeoHub:
|
|||
result = await self._send(message, reply)
|
||||
return result
|
||||
|
||||
async def set_manual(self, state: bool, devices: [NeoStat]):
|
||||
"""
|
||||
Controls the timeclock built into the neoplug, can be enabled and disabled to allow for manual operation.
|
||||
This function works only with NeoPlugs and does not work on NeoStats that are in timeclock mode.
|
||||
"""
|
||||
|
||||
names = self._devices_to_names(devices)
|
||||
message = {"MANUAL_ON" if state else "MANUAL_OFF": names}
|
||||
reply = {"result": "manual on" if state else "manual off"}
|
||||
|
||||
result = await self._send(message, reply)
|
||||
return result
|
||||
|
||||
async def set_hold(self, temperature: int, hours: int, minutes: int, devices: [NeoStat]):
|
||||
"""
|
||||
Tells a thermostat to maintain the current temperature for a fixed time.
|
||||
"""
|
||||
|
||||
names = self._devices_to_names(devices)
|
||||
ids = self._devices_to_device_ids(devices)
|
||||
message = {"HOLD": [{"temp": temperature, "hours": hours, "minutes": minutes, "id": f"{ids}"}, names]}
|
||||
reply = {"result": "temperature on hold"}
|
||||
|
||||
result = await self._send(message, reply)
|
||||
return result
|
||||
|
||||
async def set_timer_hold(self, state: bool, minutes: int, devices: [NeoStat]):
|
||||
"""
|
||||
Turns the output of timeclock on or off for certain duration
|
||||
|
@ -628,3 +628,52 @@ class NeoHub:
|
|||
|
||||
result = await self._send(message, reply)
|
||||
return result
|
||||
|
||||
async def get_engineers(self):
|
||||
"""
|
||||
Get engineers data
|
||||
"""
|
||||
message = {"GET_ENGINEERS": 0}
|
||||
|
||||
data = await self._send(message)
|
||||
return data
|
||||
|
||||
async def get_devices_data(self):
|
||||
"""
|
||||
Returns live data from hub and all devices
|
||||
"""
|
||||
|
||||
# Get live data from Neohub
|
||||
live_hub_data = await self.get_live_data()
|
||||
|
||||
# Get the engineers data from the Neohub for things like Device Type, Sensor Mode etc.
|
||||
eng_hub_data = await self.get_engineers()
|
||||
|
||||
# Remove the devices node from the hub_data.
|
||||
devices = live_hub_data.devices
|
||||
delattr(live_hub_data, "devices")
|
||||
|
||||
neo_devices = []
|
||||
|
||||
# Iterate through all devices in list returned by the get_live_data and enrich this object with the attributes
|
||||
# from engineers data.
|
||||
for device in devices:
|
||||
# Find matching device ID in Engineers data
|
||||
for engineers_data_key, engineers_data_value in eng_hub_data.__dict__.items():
|
||||
# If we found a matching device ID combine the engineering data into the live data
|
||||
if device.DEVICE_ID == engineers_data_value.DEVICE_ID:
|
||||
for x in engineers_data_value.__dict__.items():
|
||||
# FLOOR_LIMIT is a duplicate key, do not overwrite this.
|
||||
if x[0] == "FLOOR_LIMIT":
|
||||
setattr(device, "ENG_FLOOR_LIMIT", x[1])
|
||||
else:
|
||||
setattr(device, x[0], x[1])
|
||||
|
||||
for neo_device in devices:
|
||||
neo_devices.append(NeoStat(self, neo_device))
|
||||
|
||||
devices = {
|
||||
'neo_devices': neo_devices
|
||||
}
|
||||
|
||||
return devices
|
||||
|
|
|
@ -23,51 +23,56 @@ class NeoStat(SimpleNamespace):
|
|||
self._hub = hub
|
||||
|
||||
self._simple_attrs = (
|
||||
'active_level',
|
||||
'active_profile',
|
||||
'available_modes',
|
||||
'away',
|
||||
'cool_on',
|
||||
'cool_temp',
|
||||
'current_floor_temperature',
|
||||
'date',
|
||||
'device_id',
|
||||
'fan_control',
|
||||
'fan_speed',
|
||||
'floor_limit',
|
||||
'hc_mode',
|
||||
'heat_mode',
|
||||
'heat_on',
|
||||
'hold_cool',
|
||||
'fan_control',
|
||||
'fan_speed',
|
||||
'hc_mode',
|
||||
'heat_mode',
|
||||
'heat_on',
|
||||
'hold_cool',
|
||||
'hold_off',
|
||||
'hold_on',
|
||||
'hold_temp',
|
||||
'hold_time', # This is updated below.
|
||||
'holiday',
|
||||
'lock',
|
||||
'low_battery',
|
||||
'manual_off',
|
||||
'modelock',
|
||||
'modulation_level',
|
||||
'offline',
|
||||
'pin_number',
|
||||
'preheat_active',
|
||||
'prg_temp',
|
||||
'prg_timer',
|
||||
'standby',
|
||||
'switch_delay_left', # This is updated below.
|
||||
'temporary_set_flag',
|
||||
'time', # This is updated below.
|
||||
'timer_on',
|
||||
'window_open',
|
||||
'write_count'
|
||||
)
|
||||
'active_level',
|
||||
'active_profile',
|
||||
'available_modes',
|
||||
'away',
|
||||
'cool_on',
|
||||
'cool_temp',
|
||||
'current_floor_temperature',
|
||||
'date',
|
||||
'device_id',
|
||||
'device_type',
|
||||
'fan_control',
|
||||
'fan_speed',
|
||||
'floor_limit',
|
||||
'hc_mode',
|
||||
'heat_mode',
|
||||
'heat_on',
|
||||
'fan_control',
|
||||
'fan_speed',
|
||||
'hc_mode',
|
||||
'heat_mode',
|
||||
'heat_on',
|
||||
'hold_cool',
|
||||
'hold_hours',
|
||||
'hold_mins',
|
||||
'hold_off',
|
||||
'hold_on',
|
||||
'hold_state',
|
||||
'hold_temp',
|
||||
'hold_time', # This is updated below.
|
||||
'holiday',
|
||||
'lock',
|
||||
'low_battery',
|
||||
'manual_off',
|
||||
'modelock',
|
||||
'modulation_level',
|
||||
'offline',
|
||||
'pin_number',
|
||||
'preheat_active',
|
||||
'prg_temp',
|
||||
'prg_timer',
|
||||
'sensor_mode',
|
||||
'standby',
|
||||
'stat_version',
|
||||
'switch_delay_left', # This is updated below.
|
||||
'temporary_set_flag',
|
||||
'time', # This is updated below.
|
||||
'timer_on',
|
||||
'window_open',
|
||||
'write_count'
|
||||
)
|
||||
|
||||
for a in self._simple_attrs:
|
||||
data_attr = a.upper()
|
||||
|
@ -92,11 +97,31 @@ class NeoStat(SimpleNamespace):
|
|||
|
||||
_switch_delay_left = datetime.strptime(self.switch_delay_left, "%H:%M")
|
||||
self.switch_delay_left = timedelta(
|
||||
hours=_switch_delay_left.hour,
|
||||
minutes=_switch_delay_left.minute)
|
||||
hours=_switch_delay_left.hour,
|
||||
minutes=_switch_delay_left.minute)
|
||||
_time = datetime.strptime(self.time, "%H:%M")
|
||||
self.time = timedelta(hours=_time.hour, minutes=_time.minute)
|
||||
|
||||
# 35 Degrees appears to be the hard limit in app, 5 degrees appears to be the hard lower limit.
|
||||
self.max_temperature_limit = 35
|
||||
self.min_temperature_limit = 5
|
||||
|
||||
# Ensure that TIMECLOCKS are correctly reported
|
||||
if hasattr(self._data_, 'TIMECLOCK'):
|
||||
self.time_clock_mode = True
|
||||
else:
|
||||
self.time_clock_mode = False
|
||||
|
||||
# Adding an attribute to deal with battery powered or not.
|
||||
if self.device_type in [2, 5, 13, 14]:
|
||||
self.battery_powered = True
|
||||
else:
|
||||
self.battery_powered = False
|
||||
|
||||
if self.device_type == 1:
|
||||
# We're dealing with a NeoStat V1 there's a known bug in the API, so we must patch the hc_mode
|
||||
self.hc_mode = "HEATING"
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
String representation.
|
||||
|
@ -105,14 +130,14 @@ class NeoStat(SimpleNamespace):
|
|||
for elem in dir(self):
|
||||
if not callable(getattr(self, elem)) and not elem.startswith('_'):
|
||||
data_elem.append(elem)
|
||||
out = 'HeatMiser NeoStat (%s):\n' % (self.name)
|
||||
out = f'HeatMiser NeoStat {self.name}:\n'
|
||||
for elem in data_elem:
|
||||
out += ' - %s: %s\n' % (elem, getattr(self, elem))
|
||||
out += f' - {elem}: {getattr(self, elem)}\n'
|
||||
return out
|
||||
|
||||
async def identify(self):
|
||||
"""
|
||||
Flashes red LED light
|
||||
Flashes Devices LED light
|
||||
"""
|
||||
|
||||
message = {"IDENTIFY_DEV": self.name}
|
||||
|
|
|
@ -35,9 +35,8 @@ class NeoHubCLIArgumentError(Error):
|
|||
class NeoHubCLI(object):
|
||||
"""A runner for neohub_cli operations."""
|
||||
|
||||
def __init__(self, command, args, hub_ip=None, hub_token=None):
|
||||
hub_port = 4242 if hub_token is None else 4243
|
||||
self._hub = NeoHub(host=hub_ip, port=hub_port, token=hub_token)
|
||||
def __init__(self, command, args, hub_ip=None, hub_port=4242, token=None):
|
||||
self._hub = NeoHub(host=hub_ip, port=hub_port, token=token)
|
||||
self._command = command
|
||||
self._args = args
|
||||
# live data cached from the neohub. We assume this data will remain current
|
||||
|
@ -280,7 +279,9 @@ class NeoHubCLI(object):
|
|||
async def main():
|
||||
argp = argparse.ArgumentParser(description='CLI to neohub devices')
|
||||
argp.add_argument('--hub_ip', help='IP address of NeoHub', default=None)
|
||||
argp.add_argument('--hub_token', help='API token', default=None)
|
||||
argp.add_argument(
|
||||
'--hub_port', help='Port number of NeoHub to talk to', default=4242)
|
||||
argp.add_argument('--token', help='Token', default=None)
|
||||
argp.add_argument('--format', help='Output format', default='list')
|
||||
argp.add_argument('command', help='Command to issue')
|
||||
argp.add_argument('arg', help='Arguments to command', nargs='*')
|
||||
|
@ -291,8 +292,8 @@ async def main():
|
|||
args.command,
|
||||
args.arg,
|
||||
hub_ip=args.hub_ip,
|
||||
hub_token=args.hub_token,
|
||||
)
|
||||
hub_port=int(args.hub_port),
|
||||
token=args.token)
|
||||
m = await nhc.callable()
|
||||
if m:
|
||||
result = await m()
|
||||
|
|
2
setup.py
2
setup.py
|
@ -11,7 +11,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||
|
||||
setuptools.setup(
|
||||
name="neohubapi",
|
||||
version="1.1",
|
||||
version="2.0",
|
||||
description="Async library to communicate with Heatmiser NeoHub 2 API.",
|
||||
url="https://gitlab.com/neohubapi/neohubapi/",
|
||||
author="Andrius Štikonas",
|
||||
|
|
Loading…
Reference in New Issue