diff --git a/.gitignore b/.gitignore index 43091aa..158c404 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,19 @@ ENV/ .mypy_cache/ # IDE settings -.vscode/ \ No newline at end of file +.vscode/ +pyvenv.cfg +bin/activate +bin/activate_this.py +bin/activate.csh +bin/activate.fish +bin/activate.nu +bin/activate.ps1 +bin/pip +bin/pip-3.12 +bin/pip3 +bin/pip3.12 +bin/python +bin/python3 +bin/python3.12 +trane_config_davidhewing.conf diff --git a/AUTHORS.rst b/AUTHORS.rst index 1ca176c..0eadf08 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -11,4 +11,4 @@ Development Contributors ------------ -None yet. Why not be the first? +UX360 Initial Support Attempt - David Ewing diff --git a/docs/dump_home_example_ux360.json b/docs/dump_home_example_ux360.json new file mode 100755 index 0000000..3a9be7d --- /dev/null +++ b/docs/dump_home_example_ux360.json @@ -0,0 +1,774 @@ +[ + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/HOUSEID/events/collection?sys_guid=API_PROVIDED_GUID_STRING" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/HOUSEID/events?device_id=SERIALNUMBER" + }, + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER" + } + }, + "country_code": "US", + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "TSYS2C60A2VVUEA" + }, + { + "label": "Serial Number", + "type": "label_value", + "value": "SERIALNUMBER" + }, + { + "label": "AUID", + "type": "label_value", + "value": "SERIALNUMBER" + }, + { + "label": "Connection Status", + "type": "label_value", + "value": "Online" + }, + { + "label": "Main Firmware Version", + "type": "label_value", + "value": "08.01.00.240517" + } + ], + "name": "advanced_info" + }, + { + "actions": { + "unenroll": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/unenroll", + "method": "POST" + } + }, + "name": "unenroll" + }, + { + "actions": { + "set_alert_temperature_high": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/alert_temperature_high", + "method": "POST" + }, + "set_alert_temperature_low": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/alert_temperature_low", + "method": "POST" + }, + "set_setpoints": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/setpoints/1", + "method": "PUT" + } + }, + "alert_temperature_high": 120, + "alert_temperature_low": 0, + "device_identifier": "SERIALNUMBER", + "name": "thermostat", + "operating_state": "cool", + "scale": "f", + "setpoint_cool": 74, + "setpoint_cool_max": 99.0, + "setpoint_cool_min": 60, + "setpoint_delta": 3.0, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55.0, + "setpoint_increment": 1, + "status": "Cooling", + "status_icon": { + "modifiers": [], + "name": "cooling" + }, + "temperature": 74 + }, + { + "is_connected": true, + "name": "connection", + "signal_strength": "unknown" + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/mode/1", + "method": "POST" + } + }, + "display_value": "Cool", + "label": "System Mode", + "name": "thermostat_mode", + "options": [ + { + "header": true, + "id": "thermostat_mode", + "label": "System Mode", + "value": "thermostat_mode" + }, + { + "label": "Off", + "value": "off" + }, + { + "label": "Auto", + "value": "auto" + }, + { + "label": "Cool", + "value": "cool" + }, + { + "label": "Heat", + "value": "heat" + }, + { + "label": "EM Heat", + "value": "emergency_heat" + } + ], + "value": "cool" + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/run_mode/1", + "method": "POST" + } + }, + "display_value": "Hold Temp", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": true, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode" + }, + { + "id": "info_text", + "info": true, + "label": "Follow or override the schedule.", + "value": "info_text" + }, + { + "label": "Hold Temp", + "value": "hold" + }, + { + "label": "Run Schedule", + "value": "schedule" + } + ], + "value": "hold" + }, + { + "actions": { + "request_current_dealer_info": {}, + "request_dealers_by_zip": { + "href": "https://www.mynexia.com/mobile/dealers/SERIALNUMBER/search", + "method": "POST" + } + }, + "has_dealer_identifier": false, + "name": "dealer_contact_info" + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/fan_mode", + "method": "POST" + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": true, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode" + }, + { + "label": "Circulate", + "value": "circulate" + }, + { + "label": "Auto", + "value": "auto" + }, + { + "label": "On", + "value": "on" + } + ], + "status_icon": { + "modifiers": [], + "name": "thermostat_fan_on" + }, + "value": "auto" + }, + { + "actions": { + "update_thermostat_default_fan_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/fan_mode", + "method": "POST" + } + }, + "name": "thermostat_default_fan_mode", + "value": "auto" + }, + { + "is_supported": false, + "name": "gen_2_app", + "validation_failures": [ + "Thermostat model is not supported." + ] + }, + { + "actions": { + "enable_scheduling": { + "data": { + "value": true + }, + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/scheduling_enabled", + "method": "POST" + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=SERIALNUMBER", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=SERIALNUMBER", + "method": "GET" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=SERIALNUMBER", + "method": "POST" + } + }, + "can_add_remove_periods": true, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=SERIALNUMBER&house_id=HOUSEID", + "enabled": true, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1 + } + ], + "has_indoor_humidity": true, + "has_outdoor_temperature": true, + "icon": { + "modifiers": [ + "temperature-74" + ], + "name": "thermostat" + }, + "id": "SERIALNUMBER", + "indoor_humidity": 47.0, + "last_updated_at": "2024-08-08T21:20:36.000Z", + "manufacturer": "Trane", + "name": "THERMOSTAT_NAME", + "name_editable": true, + "outdoor_temperature": 90, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/mode/1" + } + }, + "current_value": "cool", + "labels": [ + "Off", + "Auto", + "Cool", + "Heat", + "EM Heat" + ], + "options": [ + { + "label": "Off", + "value": "off" + }, + { + "label": "Auto", + "value": "auto" + }, + { + "label": "Cool", + "value": "cool" + }, + { + "label": "Heat", + "value": "heat" + }, + { + "label": "EM Heat", + "value": "emergency_heat" + } + ], + "title": "Mode", + "type": "mode", + "values": [ + "off", + "auto", + "cool", + "heat", + "emergency_heat" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/run_mode/1" + } + }, + "current_value": "hold", + "labels": [ + "Hold Temp", + "Run Schedule" + ], + "options": [ + { + "label": "Hold Temp", + "value": "hold" + }, + { + "label": "Run Schedule", + "value": "schedule" + } + ], + "title": "Run Mode", + "type": "run_mode", + "values": [ + "hold", + "schedule" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/scheduling_enabled" + } + }, + "current_value": true, + "labels": [ + "ON", + "OFF" + ], + "options": [ + { + "label": "ON", + "value": true + }, + { + "label": "OFF", + "value": false + } + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [ + true, + false + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/fan_mode" + } + }, + "current_value": "auto", + "labels": [ + "Circulate", + "Auto", + "On" + ], + "options": [ + { + "label": "Circulate", + "value": "circulate" + }, + { + "label": "Auto", + "value": "auto" + }, + { + "label": "On", + "value": "on" + } + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": [ + "circulate", + "auto", + "on" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/scale" + } + }, + "current_value": "f", + "labels": [ + "F", + "C" + ], + "options": [ + { + "label": "F", + "value": "f" + }, + { + "label": "C", + "value": "c" + } + ], + "title": "Temperature Scale", + "type": "scale", + "values": [ + "f", + "c" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/high_low_alerts" + } + }, + "current_value": "0", + "labels": [ + "ON", + "OFF" + ], + "options": [ + { + "label": "ON", + "value": "1" + }, + { + "label": "OFF", + "value": "0" + } + ], + "title": "Send High and Low Temperature Alerts", + "type": "high_low_alerts", + "values": [ + "1", + "0" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/send_delta_temperature_alert" + } + }, + "current_value": "0", + "labels": [ + "ON", + "OFF" + ], + "options": [ + { + "label": "ON", + "value": "1" + }, + { + "label": "OFF", + "value": "0" + } + ], + "title": "Send Setpoint Delta Temperature Alert", + "type": "send_delta_temperature_alert", + "values": [ + "1", + "0" + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/filter_reminders_enabled" + } + }, + "current_value": false, + "labels": [ + "ON", + "OFF" + ], + "options": [ + { + "label": "ON", + "value": true + }, + { + "label": "OFF", + "value": false + } + ], + "title": "Send Filter Reminders", + "type": "filter_reminders_enabled", + "values": [ + true, + false + ] + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/filter_size" + } + }, + "current_value": null, + "labels": [ + "14x14x1", + "14x20x1", + "14x25x1", + "16x16x1", + "16x20x1", + "16x25x1", + "20x20x1", + "20x22x1", + "20x25x1", + "20x30x1" + ], + "options": [ + { + "label": "14x14x1", + "value": "14x14x1" + }, + { + "label": "14x20x1", + "value": "14x20x1" + }, + { + "label": "14x25x1", + "value": "14x25x1" + }, + { + "label": "16x16x1", + "value": "16x16x1" + }, + { + "label": "16x20x1", + "value": "16x20x1" + }, + { + "label": "16x25x1", + "value": "16x25x1" + }, + { + "label": "20x20x1", + "value": "20x20x1" + }, + { + "label": "20x22x1", + "value": "20x22x1" + }, + { + "label": "20x25x1", + "value": "20x25x1" + }, + { + "label": "20x30x1", + "value": "20x30x1" + } + ], + "title": "Filter Size", + "type": "filter_size", + "values": [ + "14x14x1", + "14x20x1", + "14x25x1", + "16x16x1", + "16x20x1", + "16x25x1", + "20x20x1", + "20x22x1", + "20x25x1", + "20x30x1" + ] + } + ], + "state_code": "AR", + "status_secondary": null, + "status_tertiary": null, + "type": "faceplate_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/zones/1" + } + }, + "cooling_setpoint": 74, + "current_zone_mode": "cool", + "features": [ + { + "actions": { + "set_setpoints": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/setpoints/1", + "method": "PUT" + } + }, + "device_identifier": "SERIALNUMBER-1", + "name": "thermostat", + "scale": "f", + "setpoint_cool": 74, + "setpoint_cool_max": 99.0, + "setpoint_cool_min": 60, + "setpoint_delta": 3.0, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55.0, + "setpoint_increment": 1, + "status": "Cooling", + "status_icon": { + "modifiers": [], + "name": "cooling" + }, + "temperature": 74 + }, + { + "is_connected": true, + "name": "connection", + "signal_strength": "unknown" + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/mode/1", + "method": "POST" + } + }, + "display_value": "Cool", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": true, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode" + }, + { + "label": "Off", + "value": "off" + }, + { + "label": "Auto", + "value": "auto" + }, + { + "label": "Cool", + "value": "cool" + }, + { + "label": "Heat", + "value": "heat" + }, + { + "label": "EM Heat", + "value": "emergency_heat" + } + ], + "value": "cool" + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/run_mode/1", + "method": "POST" + } + }, + "display_value": "Hold Temp", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": true, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode" + }, + { + "id": "info_text", + "info": true, + "label": "Follow or override the schedule.", + "value": "info_text" + }, + { + "label": "Hold Temp", + "value": "hold" + }, + { + "label": "Run Schedule", + "value": "schedule" + } + ], + "value": "hold" + }, + { + "actions": { + "enable_scheduling": { + "data": { + "value": true + }, + "href": "https://www.mynexia.com/mobile/diagnostics/thermostats/SERIALNUMBER/scheduling_enabled", + "method": "POST" + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=SERIALNUMBER-1", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=SERIALNUMBER-1", + "method": "GET" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=SERIALNUMBER-1", + "method": "POST" + } + }, + "can_add_remove_periods": true, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=SERIALNUMBER-1&house_id=HOUSEID", + "enabled": true, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1 + } + ], + "heating_setpoint": 62, + "icon": { + "modifiers": [ + "temperature-74" + ], + "name": "thermostat" + }, + "id": 1, + "name": "Zone 1", + "operating_state": "cool", + "setpoints": { + "cool": 74, + "heat": 62 + }, + "settings": [], + "temperature": 74, + "type": "thermostat_zone", + "zone_status": "cooling" + } + ] + } +] diff --git a/docs/dump_home_example_ux360.txt b/docs/dump_home_example_ux360.txt new file mode 100755 index 0000000..3898eca --- /dev/null +++ b/docs/dump_home_example_ux360.txt @@ -0,0 +1,5 @@ +Login onto the Trane provided interface provides values for: + +SERIALNUMBER +API_PROVIDED_GUID_STRING +HOUSEID \ No newline at end of file diff --git a/main.py b/main.py index 2a26c57..8ca1c55 100644 --- a/main.py +++ b/main.py @@ -6,51 +6,98 @@ import code import readline import rlcompleter +import time + +import asyncio +import aiohttp from nexia.home import NexiaHome +def displaystatus(nexia_home): + print("NexiaThermostat instance can be referenced using nt..") + print("List of available thermostats and zones:") + for _thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(_thermostat_id) + _thermostat_name = thermostat.get_name() + _thermostat_model = thermostat.get_model() + _thermostat_compressor_speed = thermostat.get_current_compressor_speed() + + print( + f'{_thermostat_id} - "{_thermostat_name}" ({_thermostat_model}) [{_thermostat_compressor_speed}]' + ) + _thermostat_firmware = thermostat.get_firmware() + print( f'Firmware: {_thermostat_firmware}' ) + _thermostat_dev_build_number = thermostat.get_dev_build_number() + print( f'Build : {_thermostat_dev_build_number}' ) + _thermostat_has_outdoor_temperature = thermostat.has_outdoor_temperature() + _thermostat_has_relative_humidity = thermostat.has_relative_humidity() + _thermostat_has_variable_speed_compressor = thermostat.has_variable_speed_compressor() + _thermostat_has_emergency_heat = thermostat.has_emergency_heat() + _thermostat_has_variable_fan_speed = thermostat.has_variable_fan_speed() + _thermostat_has_zones = thermostat.has_zones() + _thermostat_has_dehumidify_support = thermostat.has_dehumidify_support() + _thermostat_has_humidify_support = thermostat.has_humidify_support() + + print( f'Has Outdoor Temperature : {_thermostat_has_outdoor_temperature}' ) + print( f'Has Relative Humidity : {_thermostat_has_relative_humidity}' ) + print( f'Has Variable Compressor : {_thermostat_has_variable_speed_compressor}' ) + print( f'Has Emergency Heat : {_thermostat_has_emergency_heat}' ) + print( f'Has Variable Fan : {_thermostat_has_variable_fan_speed}' ) + print( f'Has Zones : {_thermostat_has_zones}' ) + print( f'Has Dehumidify Support : {_thermostat_has_dehumidify_support}' ) + print( f'Has Humidify Support : {_thermostat_has_humidify_support}' ) + + _thermostat_fanmode = thermostat.get_fan_mode() + print( f'Fan Mode : {_thermostat_fanmode}' ) + + print(" Zones:") + + for _zone_id in thermostat.get_zone_ids(): + zone = thermostat.get_zone_by_id(_zone_id) + _zone_name = zone.get_name() + _zone_status = zone.get_status() + + print(f' {_zone_id} - "{_zone_name}" ({_zone_status})') + + +async def _runner(username, password, brand): + session = aiohttp.ClientSession() + try: + nexia_home = NexiaHome( + session, username=username, password=password, brand=brand + ) + await nexia_home.login() + await nexia_home.update() + displaystatus(nexia_home) + + """ Various tests """ + # await nexia_home.thermostats[0].zones[0].set_heat_cool_temp(cool_temperature=76.0) + await nexia_home.thermostats[0].set_fan_mode("on") + + time.sleep(5) + + displaystatus(nexia_home) + + finally: + await session.close() + return nexia_home + + parser = argparse.ArgumentParser() parser.add_argument("--username", type=str, help="Your Nexia username/email address.") parser.add_argument("--password", type=str, help="Your Nexia password.") +parser.add_argument("--brand", type=str, help="Brand (nexia or asair or trane).") args = parser.parse_args() -if args.username and args.password: - nexia_home = NexiaHome(username=args.username, password=args.password) +if args.username and args.password and args.brand: + # nexia_home = NexiaHome(username=args.username, password=args.password, brand=args.brand) + nexia_home = asyncio.run(_runner(username=args.username, password=args.password, brand=args.brand)) else: parser.print_help() exit() -print("NexiaThermostat instance can be referenced using nt..") -print("List of available thermostats and zones:") -for _thermostat_id in nexia_home.get_thermostat_ids(): - thermostat = nexia_home.get_thermostat_by_id(_thermostat_id) - _thermostat_name = thermostat.get_name() - _thermostat_model = thermostat.get_model() - _thermostat_compressor_speed = thermostat.get_current_compressor_speed() - - print( - f'{_thermostat_id} - "{_thermostat_name}" ({_thermostat_model}) [{_thermostat_compressor_speed}]' - ) - print(" Zones:") - - for _zone_id in thermostat.get_zone_ids(): - zone = thermostat.get_zone_by_id(_zone_id) - _zone_name = zone.get_name() - _zone_status = zone.get_status() - - print(f' {_zone_id} - "{_zone_name}" ({_zone_status})') -del ( - _thermostat_id, - _thermostat_model, - _thermostat_name, - _zone_name, - _zone_id, - args, - parser, -) - -nexia_home.update() + variables = globals() @@ -58,4 +105,4 @@ readline.set_completer(rlcompleter.Completer(variables).complete) readline.parse_and_bind("tab: complete") -code.InteractiveConsole(variables).interact() +# code.InteractiveConsole(variables).interact() diff --git a/nexia/.github/ISSUE_TEMPLATE.md b/nexia/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 3a32ab0..0000000 --- a/nexia/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,15 +0,0 @@ -* Nexia version: -* Python version: -* Operating System: - -### Description - -Describe what you were trying to get done. -Tell us what happened, what went wrong, and what you expected to happen. - -### What I Did - -``` -Paste the command(s) you ran and the output. -If there was a crash, please include the traceback here. -``` diff --git a/nexia/__init__.py b/nexia/__init__.py old mode 100644 new mode 100755 diff --git a/nexia/automation.py b/nexia/automation.py old mode 100644 new mode 100755 diff --git a/nexia/const.py b/nexia/const.py old mode 100644 new mode 100755 diff --git a/nexia/home.py b/nexia/home.py old mode 100644 new mode 100755 index 497c244..21aec88 --- a/nexia/home.py +++ b/nexia/home.py @@ -33,6 +33,7 @@ class LoginFailedException(Exception): TIMEOUT = 20 _LOGGER = logging.getLogger(__name__) +# logging.basicConfig(level=logging.DEBUG) DEVICES_ELEMENT = 0 AUTOMATIONS_ELEMENT = 1 @@ -181,6 +182,45 @@ async def post_url(self, request_url: str, payload: dict) -> aiohttp.ClientRespo response.raise_for_status() return response + async def put_url(self, request_url: str, payload: dict) -> aiohttp.ClientResponse: + """ + puts data to the session from the url and payload + :param url: str + :param payload: dict + :return: response + """ + headers = self._api_key_headers() + _LOGGER.debug( + "put: Calling url %s with headers: %s and payload: %s", + request_url, + headers, + payload, + ) + + response: aiohttp.ClientResponse = await self.session.put( + request_url, + json=payload, + timeout=TIMEOUT, + headers=headers, + max_redirects=MAX_REDIRECTS, + ) + + _LOGGER.debug("put: Response from url %s: %s", request_url, response.content) + if response.status == 302: + # assuming its redirecting to login + _LOGGER.debug( + "put Response returned code 302, re-attempting login and resending request." + ) + await self.login() + return await self.put_url(request_url, payload) + + # no need to sleep anymore as we consume the response and update the thermostat's JSON + + response.raise_for_status() + return response + + + async def _get_url( self, request_url: str, headers: dict[str, str] | None = None ) -> aiohttp.ClientResponse: diff --git a/nexia/thermostat.py b/nexia/thermostat.py old mode 100644 new mode 100755 index b2439ca..4eca37f --- a/nexia/thermostat.py +++ b/nexia/thermostat.py @@ -1,7 +1,9 @@ """Nexia Themostat.""" + from __future__ import annotations import logging +import asyncio from typing import TYPE_CHECKING, Any from .const import AIR_CLEANER_MODES, BLOWER_OFF_STATUSES, HUMIDITY_MAX, HUMIDITY_MIN @@ -33,10 +35,12 @@ def __init__(self, nexia_home, thermostat_json): @property def API_MOBILE_THERMOSTAT_URL(self): # pylint: disable=invalid-name - return ( + _url = ( self._nexia_home.mobile_url + "/xxl_thermostats/{thermostat_id}/{end_point}" ) + return _url + @property def is_online(self): """ @@ -365,7 +369,11 @@ def get_air_cleaner_mode(self): Returns the system's air cleaner mode :return: str """ - return self.get_thermostat_settings_key("air_cleaner_mode")["current_value"] + _air_cleaner_mode = self.get_thermostat_settings_key_or_none("air_cleaner_mode") + if _air_cleaner_mode == None: + return AIR_CLEANER_MODES[0] + else: + return _air_cleaner_mode["current_value"] ######################################################################## # System Universal Set Methods @@ -672,8 +680,25 @@ async def _post_and_update_thermostat_json(self, end_point, payload): url = self.API_MOBILE_THERMOSTAT_URL.format( end_point=end_point, thermostat_id=self._thermostat_json["id"] ) + + """ Support for UX360 Thermostat """ + if self.get_model() == "TSYS2C60A2VVUEA": + print(f"UX360 End Point : {end_point}") + if end_point == "fan_mode": # update_thermostat_fan_mode + url = self._get_thermostat_deep_key( + "features", "name", "thermostat_fan_mode" + )["actions"]["update_thermostat_fan_mode"]["href"] + response = await self._nexia_home.post_url(url, payload) - self.update_thermostat_json((await response.json())["result"]) + response_json = (await response.json())["result"] + + """ Make sure that the data return is was is expected """ + if response_json.get("zones"): + self.update_thermostat_json(response_json) + else: + """ Probably means a PUT request was used -- call update """ + await asyncio.sleep(10) + await self._nexia_home.update() def update_thermostat_json(self, thermostat_json): """Update with new json from the api""" @@ -684,12 +709,18 @@ def update_thermostat_json(self, thermostat_json): "Updated thermostat_id:%s with new data from post", self.thermostat_id, ) + self._thermostat_json.update(thermostat_json) zone_updates_by_id = {} - for zone_json in thermostat_json["zones"]: - zone_updates_by_id[zone_json["id"]] = zone_json + if thermostat_json.get("zones"): + for zone_json in thermostat_json["zones"]: + zone_updates_by_id[zone_json["id"]] = zone_json - for zone in self.zones: - if zone.zone_id in zone_updates_by_id: - zone.update_zone_json(zone_updates_by_id[zone.zone_id]) + for zone in self.zones: + if zone.zone_id in zone_updates_by_id: + zone.update_zone_json(zone_updates_by_id[zone.zone_id]) + + else: + """ Probably means a PUT request was used -- call update """ + print(thermostat_json) diff --git a/nexia/util.py b/nexia/util.py old mode 100644 new mode 100755 index a72097b..9dc97ee --- a/nexia/util.py +++ b/nexia/util.py @@ -1,4 +1,5 @@ """Utils.""" + from __future__ import annotations import json @@ -27,6 +28,7 @@ def find_dict_with_keyvalue_in_json(json_dict, key_in_subdict, value_to_find): if data_group.get(key_in_subdict) == value_to_find: return data_group + print(f"Unable to find value : {value_to_find}") raise KeyError diff --git a/nexia/zone.py b/nexia/zone.py old mode 100644 new mode 100755 index e313592..fecc608 --- a/nexia/zone.py +++ b/nexia/zone.py @@ -38,7 +38,13 @@ def __init__(self, nexia_home, nexia_thermostat, zone_json): @property def API_MOBILE_ZONE_URL(self) -> str: # pylint: disable=invalid-name - return self._nexia_home.mobile_url + "/xxl_zones/{zone_id}/{end_point}" + _url = self._nexia_home.mobile_url + "/xxl_zones/{zone_id}/{end_point}" + + # Support for UX360 Thermostat + if self.thermostat.get_model() == "TSYS2C60A2VVUEA": + end_point = "{end_point}" + + return _url def get_name(self) -> str: """ @@ -504,12 +510,51 @@ def _get_zone_key(self, key: str) -> Any: return self._zone_json[key] raise KeyError(f'Zone key "{key}" invalid.') + + def _get_ux360_url(self, parent, action): + """ + Returns a tuple of the URL and HTTP Method provided by the API features + based on the parent feature and the action name + :param parent: str, action: str + :return: tuple url,method + """ + _zone_features = self._get_zone_features(parent) + + _url="" + _method="" + + if "actions" in _zone_features: + if action in _zone_features["actions"]: + _url = _zone_features["actions"][action]["href"] + _method = _zone_features["actions"][action]["method"] + + return(_url, _method) async def _post_and_update_zone_json( self, end_point: str, payload: dict[str, Any] ) -> None: url = self.API_MOBILE_ZONE_URL.format(end_point=end_point, zone_id=self.zone_id) - response = await self._nexia_home.post_url(url, payload) + method = "POST" + + """ Detection of the UX360 Trane Themostat Model """ + if self.thermostat.get_model() == "TSYS2C60A2VVUEA": + print( f'UX360 End Point : {end_point}' ) + if end_point == "run_mode": + url,method = self._get_ux360_url("thermostat_fan_mode","update_thermostat_run_mode") + + if end_point == "setpoints": + url,method = self._get_ux360_url("thermostat","set_setpoints") + + if end_point == "zone_mode": + url,method = self._get_ux360_url("thermostat_mode","update_thermostat_mode") + + if method == "POST": + response = await self._nexia_home.post_url(url, payload) + elif method == "PUT": + response = await self._nexia_home.put_url(url, payload) + else: + _LOGGER.debug("Unknown URL method") + self.update_zone_json((await response.json())["result"]) def update_zone_json(self, zone_json: dict[str, Any]) -> None: