Skip to content

Commit 7bcce77

Browse files
authored
[argoclima] Initial contribution (openhab#15481)
* Initial contribution of the ArgoClima binding Signed-off-by: Mateusz Bronk <[email protected]>
1 parent 577c73a commit 7bcce77

File tree

70 files changed

+10041
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+10041
-0
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
/bundles/org.openhab.binding.androidtv/ @morph166955
3030
/bundles/org.openhab.binding.anel/ @paphko
3131
/bundles/org.openhab.binding.anthem/ @mhilbush
32+
/bundles/org.openhab.binding.argoclima/ @mbronk
3233
/bundles/org.openhab.binding.asuswrt/ @wildcs
3334
/bundles/org.openhab.binding.astro/ @gerrieg
3435
/bundles/org.openhab.binding.atlona/ @mlobstein

bom/openhab-addons/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@
136136
<artifactId>org.openhab.binding.anthem</artifactId>
137137
<version>${project.version}</version>
138138
</dependency>
139+
<dependency>
140+
<groupId>org.openhab.addons.bundles</groupId>
141+
<artifactId>org.openhab.binding.argoclima</artifactId>
142+
<version>${project.version}</version>
143+
</dependency>
139144
<dependency>
140145
<groupId>org.openhab.addons.bundles</groupId>
141146
<artifactId>org.openhab.binding.astro</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This content is produced and maintained by the openHAB project.
2+
3+
* Project home: https://www.openhab.org
4+
5+
== Declared Project Licenses
6+
7+
This program and the accompanying materials are made available under the terms
8+
of the Eclipse Public License 2.0 which is available at
9+
https://www.eclipse.org/legal/epl-2.0/.
10+
11+
== Source Code
12+
13+
https://github.com/openhab/openhab-addons

bundles/org.openhab.binding.argoclima/README.md

+429
Large diffs are not rendered by default.
Loading
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.openhab.addons.bundles</groupId>
9+
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
10+
<version>4.2.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>org.openhab.binding.argoclima</artifactId>
14+
15+
<name>openHAB Add-ons :: Bundles :: ArgoClima Binding</name>
16+
17+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Downloads current Argo Ulisse firmware binary files from manufacturer's servers
4+
"""
5+
6+
__license__ = """This program and the accompanying materials are made available under the
7+
terms of the Eclipse Public License 2.0 which is available at
8+
http://www.eclipse.org/legal/epl-2.0
9+
10+
SPDX-License-Identifier: EPL-2.0
11+
"""
12+
13+
import hashlib
14+
import secrets
15+
import urllib.request
16+
from enum import Enum
17+
from itertools import cycle
18+
19+
20+
# Randomized values. Do not seem to impact the downloaded file
21+
USERNAME = secrets.token_hex(4)
22+
PASSWORD_MD5 = hashlib.md5(secrets.token_hex(4).encode('ASCII')).hexdigest()
23+
CPU_ID = secrets.token_hex(8)
24+
25+
26+
class FwType(str, Enum):
27+
UNIT = 'OU_FW'
28+
WIFI = 'UI_FW'
29+
30+
31+
def get_uri(fw_type: FwType, page: int):
32+
return f'http://31.14.128.210/UI/UI.php?CM={fw_type}&PK={page}&USN={USERNAME}&PSW={PASSWORD_MD5}&CPU_ID={CPU_ID}'
33+
34+
35+
def get_api_response(fw_type: FwType, page: int):
36+
with urllib.request.urlopen(get_uri(fw_type, page)) as response:
37+
data: str = response.read().decode().rstrip()
38+
if not data.endswith('|||'):
39+
raise RuntimeError(f"Invalid upstream response {data}")
40+
return {e.split('=')[0]: str.join("=", e.split('=')[1:]) for e in data[:-3].split('|')}
41+
42+
43+
def download_fw_from_remote_server(fw_type: FwType, split_into_multiple_files=False):
44+
print(f'> {get_uri(fw_type, -1)}...')
45+
ver_response = get_api_response(fw_type, -1)
46+
try:
47+
size = int(ver_response['SIZE'])
48+
chunk_count = int(ver_response['NUM_PACK'])
49+
checksum = int(ver_response['CKS']) # CRC-16?
50+
base_offset = int(ver_response['OFFSET'])
51+
print(f'FW Version: {ver_response}\n\tRelease: {ver_response["RELEASE"]}\n\tSize: {size}'
52+
f'\n\t#chunks: {chunk_count}\n\tchecksum: {checksum}')
53+
54+
total_received_size = 0
55+
data = ""
56+
current_offset = base_offset
57+
for i in range(0, chunk_count):
58+
chunk_response = get_api_response(fw_type, i)
59+
current_chunk_size_bytes = int(chunk_response['SIZE'])
60+
print(f'{fw_type} chunk [{i+1}/{chunk_count}] - Response: {chunk_response}')
61+
62+
response_offset = int(chunk_response['OFFSET'])
63+
if response_offset != current_offset:
64+
if not split_into_multiple_files:
65+
difference = response_offset - current_offset
66+
print(f"Current offset is {current_offset}, but the response wants to write to {response_offset}."
67+
f" Padding with 0xDEADBEEF")
68+
fillers = cycle(['DE', 'AD', 'BE', 'EF'])
69+
for x in range(0, difference):
70+
data += next(fillers)
71+
current_offset += difference
72+
else:
73+
save_to_file(base_offset, data, fw_type, total_received_size, ver_response["RELEASE"])
74+
total_received_size = 0
75+
data = ""
76+
current_offset = response_offset
77+
base_offset = response_offset
78+
total_received_size += current_chunk_size_bytes
79+
current_offset += current_chunk_size_bytes
80+
data += chunk_response['DATA'][:current_chunk_size_bytes*2]
81+
82+
save_to_file(base_offset, data, fw_type, total_received_size, ver_response["RELEASE"])
83+
84+
finally:
85+
finish_response = get_api_response(fw_type, 256)
86+
print(finish_response)
87+
88+
89+
def save_to_file(base_offset, data, fw_type, total_received_size, version):
90+
print()
91+
print('-' * 50)
92+
print(f'Received {total_received_size} bytes. Total binary size: {len(data) / 2:.0f}[b]')
93+
print(f'Data (base16):\n\t{data}\n')
94+
fw_binary = bytes.fromhex(data)
95+
filename = f'Argo_firmware_{fw_type}_v{version}__offset_0x{base_offset:X}.bin'
96+
with open(filename, "wb") as output_file:
97+
output_file.write(fw_binary)
98+
print(f'Firmware written to {filename}')
99+
100+
101+
if __name__ == '__main__':
102+
print(f'Username={USERNAME}, Password={PASSWORD_MD5}, CPU_ID={CPU_ID}')
103+
download_fw_from_remote_server(fw_type=FwType.UNIT, split_into_multiple_files=False)
104+
download_fw_from_remote_server(fw_type=FwType.WIFI, split_into_multiple_files=False)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<features name="org.openhab.binding.argoclima-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
3+
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
4+
5+
<feature name="openhab-binding-argoclima" description="ArgoClima Binding" version="${project.version}">
6+
<feature>openhab-runtime-base</feature>
7+
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.argoclima/${project.version}</bundle>
8+
</feature>
9+
</features>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.argoclima.internal;
14+
15+
import java.time.Duration;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.openhab.core.thing.ThingTypeUID;
19+
20+
/**
21+
* The {@link ArgoClimaBindingConstants} class defines common constants, which are
22+
* used across the whole binding.
23+
*
24+
* @author Mateusz Bronk - Initial contribution
25+
*/
26+
@NonNullByDefault
27+
public class ArgoClimaBindingConstants {
28+
29+
public static final String BINDING_ID = "argoclima";
30+
31+
/////////////
32+
// List of all Thing Type UIDs
33+
/////////////
34+
public static final ThingTypeUID THING_TYPE_ARGOCLIMA_LOCAL = new ThingTypeUID(BINDING_ID, "local");
35+
public static final ThingTypeUID THING_TYPE_ARGOCLIMA_REMOTE = new ThingTypeUID(BINDING_ID, "remote");
36+
37+
/////////////
38+
// Thing configuration parameters
39+
/////////////
40+
public static final String PARAMETER_HOSTNAME = "hostname";
41+
public static final String PARAMETER_LOCAL_DEVICE_IP = "localDeviceIP";
42+
public static final String PARAMETER_HVAC_LISTEN_PORT = "hvacListenPort";
43+
public static final String PARAMETER_DEVICE_CPU_ID = "deviceCpuId";
44+
public static final String PARAMETER_CONNECTION_MODE = "connectionMode"; // LOCAL_CONNECTION | REMOTE_API_STUB |
45+
// REMOTE_API_PROXY
46+
public static final String PARAMETER_USE_LOCAL_CONNECTION = "useLocalConnection";
47+
public static final String PARAMETER_REFRESH_INTERNAL = "refreshInterval";
48+
public static final String PARAMETER_STUB_SERVER_PORT = "stubServerPort";
49+
public static final String PARAMETER_STUB_SERVER_LISTEN_ADDRESSES = "stubServerListenAddresses";
50+
public static final String PARAMETER_OEM_SERVER_PORT = "oemServerPort";
51+
public static final String PARAMETER_OEM_SERVER_ADDRESS = "oemServerAddress";
52+
public static final String PARAMETER_INCLUDE_DEVICE_SIDE_PASSWORDS_IN_PROPERTIES = "includeDeviceSidePasswordsInProperties";
53+
public static final String PARAMETER_MATCH_ANY_INCOMING_DEVICE_IP = "matchAnyIncomingDeviceIp";
54+
55+
public static final String PARAMETER_USERNAME = "username";
56+
public static final String PARAMETER_PASSWORD = "password";
57+
58+
public static final String PARAMETER_SCHEDULE_GROUP_NAME = "schedule%d"; // 1..3
59+
public static final String PARAMETER_SCHEDULE_X_DAYS = PARAMETER_SCHEDULE_GROUP_NAME + "DayOfWeek";
60+
public static final String PARAMETER_SCHEDULE_X_ON_TIME = PARAMETER_SCHEDULE_GROUP_NAME + "OnTime";
61+
public static final String PARAMETER_SCHEDULE_X_OFF_TIME = PARAMETER_SCHEDULE_GROUP_NAME + "OffTime";
62+
public static final String PARAMETER_ACTIONS_GROUP_NAME = "actions";
63+
public static final String PARAMETER_RESET_TO_FACTORY_DEFAULTS = "resetToFactoryDefaults";
64+
65+
/////////////
66+
// Thing configuration properties
67+
/////////////
68+
public static final String PROPERTY_CPU_ID = "cpuId";
69+
public static final String PROPERTY_LOCAL_IP_ADDRESS = "localIpAddress";
70+
public static final String PROPERTY_UNIT_FW = "unitFirmwareVersion";
71+
public static final String PROPERTY_WIFI_FW = "wifiFirmwareVersion";
72+
public static final String PROPERTY_LAST_SEEN = "lastSeen";
73+
public static final String PROPERTY_WEB_UI = "argoWebUI";
74+
public static final String PROPERTY_WEB_UI_USERNAME = "argoWebUIUsername";
75+
public static final String PROPERTY_WEB_UI_PASSWORD = "argoWebUIPassword";
76+
public static final String PROPERTY_WIFI_SSID = "wifiSSID";
77+
public static final String PROPERTY_WIFI_PASSWORD = "wifiPassword";
78+
public static final String PROPERTY_LOCAL_TIME = "localTime";
79+
80+
/////////////
81+
// List of all Channel IDs
82+
/////////////
83+
public static final String CHANNEL_POWER = "ac-controls#power";
84+
public static final String CHANNEL_MODE = "ac-controls#mode";
85+
public static final String CHANNEL_SET_TEMPERATURE = "ac-controls#set-temperature";
86+
public static final String CHANNEL_CURRENT_TEMPERATURE = "ac-controls#current-temperature";
87+
public static final String CHANNEL_FAN_SPEED = "ac-controls#fan-speed";
88+
public static final String CHANNEL_ECO_MODE = "modes#eco-mode";
89+
public static final String CHANNEL_TURBO_MODE = "modes#turbo-mode";
90+
public static final String CHANNEL_NIGHT_MODE = "modes#night-mode";
91+
public static final String CHANNEL_ACTIVE_TIMER = "timers#active-timer";
92+
public static final String CHANNEL_DELAY_TIMER = "timers#delay-timer";
93+
// Note: schedule timers day of week/time setting not currently supported as channels (YAGNI), and moved to config
94+
public static final String CHANNEL_MODE_EX = "unsupported#mode-ex";
95+
public static final String CHANNEL_SWING_MODE = "unsupported#swing-mode";
96+
public static final String CHANNEL_FILTER_MODE = "unsupported#filter-mode";
97+
98+
public static final String CHANNEL_I_FEEL_ENABLED = "settings#ifeel-enabled";
99+
public static final String CHANNEL_DEVICE_LIGHTS = "settings#device-lights";
100+
101+
public static final String CHANNEL_TEMPERATURE_DISPLAY_UNIT = "settings#temperature-display-unit";
102+
public static final String CHANNEL_ECO_POWER_LIMIT = "settings#eco-power-limit";
103+
104+
/////////////
105+
// Binding's hard-coded configuration (not parameterized)
106+
/////////////
107+
/** Maximum number of failed status polls after which the device will be considered offline */
108+
public static final int MAX_API_RETRIES = 3;
109+
110+
/**
111+
* Time to wait between command issue and communicating with the device. Allows to include multiple commands in one
112+
* device communication session (preferred).
113+
* Time window chosen so that it is not (too) perceptible by an user, while still enough for rules/groups to be able
114+
* to fit
115+
*/
116+
public static final Duration SEND_COMMAND_DEBOUNCE_TIME = Duration.ofMillis(100);
117+
118+
/**
119+
* The minimum resolution during which the command sending background thread does any meaningful action. This is
120+
* merely to avoid busy wait and doesn't mean the thread is doing anything of use on every cycle. There are separate
121+
* configurable "update" and "(re)send" frequencies governing that. This parameter only controls the lowest possible
122+
* resolution of those (a "tick")
123+
*/
124+
public static final Duration SEND_COMMAND_DUTY_CYCLE = Duration.ofSeconds(1);
125+
126+
/**
127+
* The frequency to poll the device with, waiting for the command confirmation
128+
*/
129+
public static final Duration POLL_FREQUENCY_AFTER_COMMAND_SENT_LOCAL = Duration.ofSeconds(3);
130+
131+
/**
132+
* The frequency to poll the Argo servers with, waiting for the command confirmation
133+
*/
134+
public static final Duration POLL_FREQUENCY_AFTER_COMMAND_SENT_REMOTE = Duration.ofSeconds(5);
135+
136+
/**
137+
* The frequency to re-send the pending command to the device at (if it hadn't been confirmed yet).
138+
* Aka. the optimistic time when the device "should acknowledge. Should be greater than
139+
* {@link #POLL_FREQUENCY_AFTER_COMMAND_SENT_LOCAL}
140+
*
141+
* @see #SEND_COMMAND_MAX_WAIT_TIME_LOCAL_DIRECT
142+
* @see #SEND_COMMAND_MAX_WAIT_TIME_LOCAL_INDIRECT
143+
*/
144+
public static final Duration SEND_COMMAND_RETRY_FREQUENCY_LOCAL = Duration.ofSeconds(10);
145+
146+
/**
147+
* The frequency to re-send the pending command to the remote Argo server at (if it hadn't been confirmed yet).
148+
* Aka. the optimistic time when the server "should acknowledge. Should be greater than
149+
* {@link #POLL_FREQUENCY_AFTER_COMMAND_SENT_REMOTE}
150+
*
151+
* @see #SEND_COMMAND_MAX_WAIT_TIME_REMOTE
152+
*/
153+
public static final Duration SEND_COMMAND_RETRY_FREQUENCY_REMOTE = Duration.ofSeconds(20);
154+
155+
/**
156+
* Max time to wait for a pending command to be confirmed by the device in a local-direct mode (when we are issuing
157+
* communications to a device in local LAN).
158+
* <p>
159+
* During this time, the commands may get {@link #SEND_COMMAND_RETRY_FREQUENCY_LOCAL retried} and the device status
160+
* may be
161+
* {@link #POLL_FREQUENCY_AFTER_COMMAND_SENT_LOCAL re-fetched}
162+
*/
163+
public static final Duration SEND_COMMAND_MAX_WAIT_TIME_LOCAL_DIRECT = Duration.ofSeconds(20); // 60-remote
164+
165+
/**
166+
* Max time to wait for a pending command to be confirmed in an *indirect* mode (where we're only
167+
* sniffing/intercepting communications)
168+
* <p>
169+
* A healthy device seems to be polling Argo servers every minute (and if the server returns a pending command
170+
* request, does a few more more frequent exchanges as well), so 2 minutes seem safe
171+
*/
172+
public static final Duration SEND_COMMAND_MAX_WAIT_TIME_LOCAL_INDIRECT = Duration.ofSeconds(120);
173+
174+
/**
175+
* Max time to wait for a pending command to be confirmed in an *remote* mode (where we're talking to a remote Argo
176+
* server)
177+
* <p>
178+
* The server seems to confirm a bit faster than our intercepting proxy and we want to minimize traffic our binding
179+
* issues against remote side, hence a more conservative value
180+
*/
181+
public static final Duration SEND_COMMAND_MAX_WAIT_TIME_REMOTE = Duration.ofSeconds(60);
182+
183+
/**
184+
* Time to wait for (confirmable) command to be reported back by the device (by changing its state to the requested
185+
* value). If this period elapses w/o the device confirming, the command is considered not handled and REJECTED
186+
* (would not be retried any more, and the reported device's state will be the actual one device sent, not the
187+
* "in-flight" desired one)
188+
*
189+
* @implNote This is just a final "give up" time (not affecting any send logic). Should be no shorter than max try
190+
* time
191+
*/
192+
public static final Duration PENDING_COMMAND_EXPIRE_TIME = SEND_COMMAND_MAX_WAIT_TIME_LOCAL_INDIRECT
193+
.plus(Duration.ofSeconds(1));
194+
195+
/**
196+
* Timeout for getting the HTTP response from Argo servers in pass-through(proxy) mode
197+
*/
198+
public static final Duration UPSTREAM_PROXY_HTTP_REQUEST_TIMEOUT = Duration.ofSeconds(30);
199+
200+
/////////////
201+
// R&D-only switches
202+
/////////////
203+
/**
204+
* Whether the binding shall wait for the device confirming commands have been received (by flipping to the desired
205+
* state) or work in a fire and forget mode and stop tracking upon first send.
206+
* <p>
207+
* This applies only to confirmable commands (read-write) and is a default behavior of Argo's own web implementation
208+
*
209+
* @implNote This is a debug-only switch (makes little to no sense to disable it in real-world usage)
210+
*/
211+
public static final boolean AWAIT_DEVICE_CONFIRMATIONS_AFTER_COMMANDS = true;
212+
}

0 commit comments

Comments
 (0)