Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -34,9 +34,8 @@ async def run():
|
||||||
# Or, for a websocket connection:
|
# Or, for a websocket connection:
|
||||||
# hub = neohub.Neohub(port=4243, token='xxx-xxxxxxx')
|
# hub = neohub.Neohub(port=4243, token='xxx-xxxxxxx')
|
||||||
system = await hub.get_system()
|
system = await hub.get_system()
|
||||||
hub_data = await hub.get_devices_data()
|
hub_data, devices = await hub.get_live_data()
|
||||||
devices = hub_data['neo_devices']
|
for device in devices['thermostats']:
|
||||||
for device in devices:
|
|
||||||
print(f"Temperature in zone {device.name}: {device.temperature}")
|
print(f"Temperature in zone {device.name}: {device.temperature}")
|
||||||
await device.identify()
|
await device.identify()
|
||||||
|
|
||||||
|
@ -55,5 +54,5 @@ $ neohub_cli.py help # Shows all commands
|
||||||
$ neohub_cli.py help set_time # Displays help for the set_time function
|
$ neohub_cli.py help set_time # Displays help for the set_time function
|
||||||
$ neohub_cli.py --hub_ip=myneohub set_time "2021-01-31 15:43:00" # Specify times like this
|
$ neohub_cli.py --hub_ip=myneohub set_time "2021-01-31 15:43:00" # Specify times like this
|
||||||
$ neohub_cli.py --hub_ip=myneohub set_lock 1234 "Living Room" # Name NeoStats like this.
|
$ neohub_cli.py --hub_ip=myneohub set_lock 1234 "Living Room" # Name NeoStats like this.
|
||||||
$ neohub_cli.py --hub_ip=myneohub --hub_token=XXX get_system # Get system variables with websocket connection
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class NeoHub:
|
||||||
def __init__(self, host='Neo-Hub', port=4242, request_timeout=60, request_attempts=1, token=None):
|
def __init__(self, host='Neo-Hub', port=4242, request_timeout=60, request_attempts=1, token=None):
|
||||||
self._logger = logging.getLogger('neohub')
|
self._logger = logging.getLogger('neohub')
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = int(port)
|
self._port = port
|
||||||
self._request_timeout = request_timeout
|
self._request_timeout = request_timeout
|
||||||
self._request_attempts = request_attempts
|
self._request_attempts = request_attempts
|
||||||
self._token = token
|
self._token = token
|
||||||
|
@ -67,7 +67,7 @@ class NeoHub:
|
||||||
async def _send(self, message, expected_reply=None):
|
async def _send(self, message, expected_reply=None):
|
||||||
last_exception = None
|
last_exception = None
|
||||||
writer = None
|
writer = None
|
||||||
for attempt in range(1, self._request_attempts + 1):
|
for attempt in range(1, self._request_attempts+1):
|
||||||
try:
|
try:
|
||||||
if self._token is not None:
|
if self._token is not None:
|
||||||
# Websocket connection on port 4243, introduced in v3 of the API on 12/12/21
|
# 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")
|
self._logger.debug("Using legacy connection")
|
||||||
reader, writer = await asyncio.open_connection(self._host, self._port)
|
reader, writer = await asyncio.open_connection(self._host, self._port)
|
||||||
data = await asyncio.wait_for(
|
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')
|
json_string = data.decode('utf-8')
|
||||||
|
|
||||||
self._logger.debug(f"Received message: {json_string}")
|
self._logger.debug(f"Received message: {json_string}")
|
||||||
|
@ -133,15 +133,6 @@ class NeoHub:
|
||||||
raise last_exception
|
raise last_exception
|
||||||
return False
|
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]):
|
def _devices_to_names(self, devices: [NeoStat]):
|
||||||
"""
|
"""
|
||||||
Returns the list of device names
|
Returns the list of device names
|
||||||
|
@ -299,9 +290,9 @@ class NeoHub:
|
||||||
|
|
||||||
result = await self._send(message)
|
result = await self._send(message)
|
||||||
result.start = datetime.datetime.strptime(
|
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 = 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
|
return result
|
||||||
|
|
||||||
async def cancel_holiday(self):
|
async def cancel_holiday(self):
|
||||||
|
@ -444,9 +435,27 @@ class NeoHub:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
message = {"GET_LIVE_DATA": 0}
|
message = {"GET_LIVE_DATA": 0}
|
||||||
live_data = await self._send(message)
|
|
||||||
|
|
||||||
return live_data
|
hub_data = await self._send(message)
|
||||||
|
devices = hub_data.devices
|
||||||
|
delattr(hub_data, "devices")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
async def permit_join(self, name, timeout_s: int = 120):
|
async def permit_join(self, name, timeout_s: int = 120):
|
||||||
"""
|
"""
|
||||||
|
@ -589,32 +598,6 @@ class NeoHub:
|
||||||
result = await self._send(message, reply)
|
result = await self._send(message, reply)
|
||||||
return result
|
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]):
|
async def set_timer_hold(self, state: bool, minutes: int, devices: [NeoStat]):
|
||||||
"""
|
"""
|
||||||
Turns the output of timeclock on or off for certain duration
|
Turns the output of timeclock on or off for certain duration
|
||||||
|
@ -628,52 +611,3 @@ class NeoHub:
|
||||||
|
|
||||||
result = await self._send(message, reply)
|
result = await self._send(message, reply)
|
||||||
return result
|
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,56 +23,51 @@ class NeoStat(SimpleNamespace):
|
||||||
self._hub = hub
|
self._hub = hub
|
||||||
|
|
||||||
self._simple_attrs = (
|
self._simple_attrs = (
|
||||||
'active_level',
|
'active_level',
|
||||||
'active_profile',
|
'active_profile',
|
||||||
'available_modes',
|
'available_modes',
|
||||||
'away',
|
'away',
|
||||||
'cool_on',
|
'cool_on',
|
||||||
'cool_temp',
|
'cool_temp',
|
||||||
'current_floor_temperature',
|
'current_floor_temperature',
|
||||||
'date',
|
'date',
|
||||||
'device_id',
|
'device_id',
|
||||||
'device_type',
|
'fan_control',
|
||||||
'fan_control',
|
'fan_speed',
|
||||||
'fan_speed',
|
'floor_limit',
|
||||||
'floor_limit',
|
'hc_mode',
|
||||||
'hc_mode',
|
'heat_mode',
|
||||||
'heat_mode',
|
'heat_on',
|
||||||
'heat_on',
|
'hold_cool',
|
||||||
'fan_control',
|
'fan_control',
|
||||||
'fan_speed',
|
'fan_speed',
|
||||||
'hc_mode',
|
'hc_mode',
|
||||||
'heat_mode',
|
'heat_mode',
|
||||||
'heat_on',
|
'heat_on',
|
||||||
'hold_cool',
|
'hold_cool',
|
||||||
'hold_hours',
|
'hold_off',
|
||||||
'hold_mins',
|
'hold_on',
|
||||||
'hold_off',
|
'hold_temp',
|
||||||
'hold_on',
|
'hold_time', # This is updated below.
|
||||||
'hold_state',
|
'holiday',
|
||||||
'hold_temp',
|
'lock',
|
||||||
'hold_time', # This is updated below.
|
'low_battery',
|
||||||
'holiday',
|
'manual_off',
|
||||||
'lock',
|
'modelock',
|
||||||
'low_battery',
|
'modulation_level',
|
||||||
'manual_off',
|
'offline',
|
||||||
'modelock',
|
'pin_number',
|
||||||
'modulation_level',
|
'preheat_active',
|
||||||
'offline',
|
'prg_temp',
|
||||||
'pin_number',
|
'prg_timer',
|
||||||
'preheat_active',
|
'standby',
|
||||||
'prg_temp',
|
'switch_delay_left', # This is updated below.
|
||||||
'prg_timer',
|
'temporary_set_flag',
|
||||||
'sensor_mode',
|
'time', # This is updated below.
|
||||||
'standby',
|
'timer_on',
|
||||||
'stat_version',
|
'window_open',
|
||||||
'switch_delay_left', # This is updated below.
|
'write_count'
|
||||||
'temporary_set_flag',
|
)
|
||||||
'time', # This is updated below.
|
|
||||||
'timer_on',
|
|
||||||
'window_open',
|
|
||||||
'write_count'
|
|
||||||
)
|
|
||||||
|
|
||||||
for a in self._simple_attrs:
|
for a in self._simple_attrs:
|
||||||
data_attr = a.upper()
|
data_attr = a.upper()
|
||||||
|
@ -97,31 +92,11 @@ class NeoStat(SimpleNamespace):
|
||||||
|
|
||||||
_switch_delay_left = datetime.strptime(self.switch_delay_left, "%H:%M")
|
_switch_delay_left = datetime.strptime(self.switch_delay_left, "%H:%M")
|
||||||
self.switch_delay_left = timedelta(
|
self.switch_delay_left = timedelta(
|
||||||
hours=_switch_delay_left.hour,
|
hours=_switch_delay_left.hour,
|
||||||
minutes=_switch_delay_left.minute)
|
minutes=_switch_delay_left.minute)
|
||||||
_time = datetime.strptime(self.time, "%H:%M")
|
_time = datetime.strptime(self.time, "%H:%M")
|
||||||
self.time = timedelta(hours=_time.hour, minutes=_time.minute)
|
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):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
String representation.
|
String representation.
|
||||||
|
@ -130,14 +105,14 @@ class NeoStat(SimpleNamespace):
|
||||||
for elem in dir(self):
|
for elem in dir(self):
|
||||||
if not callable(getattr(self, elem)) and not elem.startswith('_'):
|
if not callable(getattr(self, elem)) and not elem.startswith('_'):
|
||||||
data_elem.append(elem)
|
data_elem.append(elem)
|
||||||
out = f'HeatMiser NeoStat {self.name}:\n'
|
out = 'HeatMiser NeoStat (%s):\n' % (self.name)
|
||||||
for elem in data_elem:
|
for elem in data_elem:
|
||||||
out += f' - {elem}: {getattr(self, elem)}\n'
|
out += ' - %s: %s\n' % (elem, getattr(self, elem))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
async def identify(self):
|
async def identify(self):
|
||||||
"""
|
"""
|
||||||
Flashes Devices LED light
|
Flashes red LED light
|
||||||
"""
|
"""
|
||||||
|
|
||||||
message = {"IDENTIFY_DEV": self.name}
|
message = {"IDENTIFY_DEV": self.name}
|
||||||
|
|
|
@ -35,8 +35,8 @@ class NeoHubCLIArgumentError(Error):
|
||||||
class NeoHubCLI(object):
|
class NeoHubCLI(object):
|
||||||
"""A runner for neohub_cli operations."""
|
"""A runner for neohub_cli operations."""
|
||||||
|
|
||||||
def __init__(self, command, args, hub_ip=None, hub_port=4242, token=None):
|
def __init__(self, command, args, hub_ip=None, hub_port=4242):
|
||||||
self._hub = NeoHub(host=hub_ip, port=hub_port, token=token)
|
self._hub = NeoHub(host=hub_ip, port=hub_port)
|
||||||
self._command = command
|
self._command = command
|
||||||
self._args = args
|
self._args = args
|
||||||
# live data cached from the neohub. We assume this data will remain current
|
# live data cached from the neohub. We assume this data will remain current
|
||||||
|
@ -90,9 +90,9 @@ class NeoHubCLI(object):
|
||||||
arg_types = []
|
arg_types = []
|
||||||
for p in sig.parameters:
|
for p in sig.parameters:
|
||||||
a = sig.parameters[p].annotation
|
a = sig.parameters[p].annotation
|
||||||
if isinstance(a, type):
|
if type(a) == type:
|
||||||
arg_types.append(a)
|
arg_types.append(a)
|
||||||
elif isinstance(a, list):
|
elif type(a) == list:
|
||||||
if a[0] == NeoStat:
|
if a[0] == NeoStat:
|
||||||
arg_types.append(a[0])
|
arg_types.append(a[0])
|
||||||
else:
|
else:
|
||||||
|
@ -203,14 +203,14 @@ class NeoHubCLI(object):
|
||||||
if special_case:
|
if special_case:
|
||||||
return special_case(raw_result, output_format)
|
return special_case(raw_result, output_format)
|
||||||
|
|
||||||
if isinstance(raw_result, bool):
|
if type(raw_result) == bool:
|
||||||
return 'Command Succeeded' if raw_result else 'Command Failed'
|
return 'Command Succeeded' if raw_result else 'Command Failed'
|
||||||
|
|
||||||
if type(raw_result) in (int, str):
|
if type(raw_result) in (int, str):
|
||||||
return f'{raw_result}'
|
return f'{raw_result}'
|
||||||
|
|
||||||
if not isinstance(raw_result, types.SimpleNamespace):
|
if type(raw_result) != types.SimpleNamespace:
|
||||||
if isinstance(raw_result, dict):
|
if type(raw_result) == dict:
|
||||||
raw_result = types.SimpleNamespace(**raw_result)
|
raw_result = types.SimpleNamespace(**raw_result)
|
||||||
else:
|
else:
|
||||||
raise NeoHubCLIInternalError(
|
raise NeoHubCLIInternalError(
|
||||||
|
@ -224,7 +224,7 @@ class NeoHubCLI(object):
|
||||||
"""
|
"""
|
||||||
if type(val) in (ScheduleFormat, Weekday):
|
if type(val) in (ScheduleFormat, Weekday):
|
||||||
return val.value
|
return val.value
|
||||||
elif isinstance(val, NeoStat):
|
elif type(val) == NeoStat:
|
||||||
return f'[NeoStat: {val.name}]'
|
return f'[NeoStat: {val.name}]'
|
||||||
else:
|
else:
|
||||||
return val
|
return val
|
||||||
|
@ -281,7 +281,6 @@ async def main():
|
||||||
argp.add_argument('--hub_ip', help='IP address of NeoHub', default=None)
|
argp.add_argument('--hub_ip', help='IP address of NeoHub', default=None)
|
||||||
argp.add_argument(
|
argp.add_argument(
|
||||||
'--hub_port', help='Port number of NeoHub to talk to', default=4242)
|
'--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('--format', help='Output format', default='list')
|
||||||
argp.add_argument('command', help='Command to issue')
|
argp.add_argument('command', help='Command to issue')
|
||||||
argp.add_argument('arg', help='Arguments to command', nargs='*')
|
argp.add_argument('arg', help='Arguments to command', nargs='*')
|
||||||
|
@ -292,8 +291,7 @@ async def main():
|
||||||
args.command,
|
args.command,
|
||||||
args.arg,
|
args.arg,
|
||||||
hub_ip=args.hub_ip,
|
hub_ip=args.hub_ip,
|
||||||
hub_port=int(args.hub_port),
|
hub_port=args.hub_port)
|
||||||
token=args.token)
|
|
||||||
m = await nhc.callable()
|
m = await nhc.callable()
|
||||||
if m:
|
if m:
|
||||||
result = await 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(
|
setuptools.setup(
|
||||||
name="neohubapi",
|
name="neohubapi",
|
||||||
version="2.0",
|
version="0.9",
|
||||||
description="Async library to communicate with Heatmiser NeoHub 2 API.",
|
description="Async library to communicate with Heatmiser NeoHub 2 API.",
|
||||||
url="https://gitlab.com/neohubapi/neohubapi/",
|
url="https://gitlab.com/neohubapi/neohubapi/",
|
||||||
author="Andrius Štikonas",
|
author="Andrius Štikonas",
|
||||||
|
|
Loading…
Reference in New Issue