2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.argoclima.internal.device.api;
16 import java.time.DateTimeException;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.time.OffsetDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.format.FormatStyle;
22 import java.util.Collections;
23 import java.util.Optional;
24 import java.util.SortedMap;
25 import java.util.TreeMap;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.openhab.binding.argoclima.internal.ArgoClimaBindingConstants;
29 import org.openhab.binding.argoclima.internal.ArgoClimaTranslationProvider;
30 import org.openhab.binding.argoclima.internal.configuration.ArgoClimaConfigurationRemote;
31 import org.openhab.binding.argoclima.internal.device.passthrough.requests.DeviceSideUpdateDTO;
32 import org.openhab.binding.argoclima.internal.exception.ArgoApiCommunicationException;
33 import org.openhab.core.i18n.TimeZoneProvider;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * Represents the current device status, as-communicated by the device (either push or pull model)
40 * Includes both the "raw" {@link #getCommandString() commandString} as well as {@link #getProperties() properties}
42 * @author Mateusz Bronk - Initial contribution
50 * Helper class for dealing with device properties
52 * @author Mateusz Bronk - Initial contribution
54 static class DeviceProperties {
55 private static final Logger LOGGER = LoggerFactory.getLogger(DeviceProperties.class);
56 private final Optional<String> localIP;
57 private final Optional<OffsetDateTime> lastSeen;
58 private final Optional<URL> vendorUiUrl;
59 private Optional<String> cpuId = Optional.empty();
60 private Optional<String> webUiUsername = Optional.empty();
61 private Optional<String> webUiPassword = Optional.empty();
62 private Optional<String> unitFWVersion = Optional.empty();
63 private Optional<String> wifiFWVersion = Optional.empty();
64 private Optional<String> wifiSSID = Optional.empty();
65 private Optional<String> wifiPassword = Optional.empty();
66 private Optional<String> localTime = Optional.empty();
69 * C-tor (from remote server query response)
71 * @param localIP The local IP of the Argo device (or empty string if N/A)
72 * @param lastSeenStr The ISO-8601-formatted date/time of last update (or empty string if N/A)
73 * @param vendorUiAddress The optional full URL to vendor's web UI
75 public DeviceProperties(String localIP, String lastSeenStr, Optional<URL> vendorUiAddress) {
76 this.localIP = localIP.isEmpty() ? Optional.empty() : Optional.of(localIP);
77 this.vendorUiUrl = vendorUiAddress;
78 this.lastSeen = dateFromISOString(lastSeenStr, "LastSeen");
82 * C-tor (from live poll response)
84 * @param lastSeen The date/time of last update (when the response got received)
86 public DeviceProperties(OffsetDateTime lastSeen) {
87 this.localIP = Optional.empty();
88 this.lastSeen = Optional.of(lastSeen);
89 this.vendorUiUrl = Optional.empty();
93 * C-tor (from intercepted device-side query to remote)
95 * @param lastSeen The date/time of last update (when the message got intercepted)
96 * @param properties The intercepted device-side request (most rich with properties)
98 public DeviceProperties(OffsetDateTime lastSeen, DeviceSideUpdateDTO properties) {
99 this.localIP = Optional.of(properties.setup.localIP.orElse(properties.deviceIp));
100 this.lastSeen = Optional.of(lastSeen);
101 this.vendorUiUrl = Optional.of(ArgoClimaRemoteDevice.getWebUiUrl(properties.remoteServerId, 80));
102 this.cpuId = Optional.of(properties.cpuId);
103 this.webUiUsername = Optional.of(properties.setup.username.orElse(properties.username));
104 this.webUiPassword = properties.setup.password;
105 this.unitFWVersion = Optional.of(properties.setup.unitVersionInstalled.orElse(properties.unitFirmware));
106 this.wifiFWVersion = Optional.of(properties.setup.wifiVersionInstalled.orElse(properties.wifiFirmware));
107 this.wifiSSID = properties.setup.wifiSSID;
108 this.wifiPassword = properties.setup.wifiPassword;
109 this.localTime = properties.setup.localTime;
112 private static Optional<OffsetDateTime> dateFromISOString(String isoDateTime, String contextualName) {
113 if (isoDateTime.isEmpty()) {
114 return Optional.empty();
118 return Optional.of(OffsetDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(isoDateTime)));
119 } catch (DateTimeException ex) {
120 // Swallowing exception (no need to handle - proceed as if the date was never provided)
121 LOGGER.debug("Failed to parse [{}] timestamp: {}. Exception: {}", contextualName, isoDateTime,
123 return Optional.empty();
127 private static String dateTimeToStringLocal(OffsetDateTime toConvert, TimeZoneProvider timeZoneProvider) {
128 var timeAtZone = toConvert.atZoneSameInstant(timeZoneProvider.getTimeZone());
129 return timeAtZone.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG));
133 * Returns duration between last update and now. If last update is N/A, picking lowest possible time value
135 * @return Time elapsed since last device-side update
137 Duration getLastSeenDelta() {
138 return Duration.between(lastSeen.orElse(OffsetDateTime.MIN).toInstant(), Instant.now());
142 * Return the properties in a map (ready to pass on to openHAB engine)
144 * @param timeZoneProvider TZ provider, for parsing date/time values
145 * @return Properties map
147 SortedMap<String, String> asPropertiesRaw(TimeZoneProvider timeZoneProvider) {
148 var result = new TreeMap<String, String>();
150 this.lastSeen.map((value) -> result.put(ArgoClimaBindingConstants.PROPERTY_LAST_SEEN,
151 dateTimeToStringLocal(value, timeZoneProvider)));
152 this.localIP.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_LOCAL_IP_ADDRESS, value));
153 this.vendorUiUrl.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WEB_UI, value.toString()));
154 this.cpuId.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_CPU_ID, value));
155 this.webUiUsername.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WEB_UI_USERNAME, value));
156 this.webUiPassword.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WEB_UI_PASSWORD, value));
157 this.unitFWVersion.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_UNIT_FW, value));
158 this.wifiFWVersion.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WIFI_FW, value));
159 this.wifiSSID.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WIFI_SSID, value));
160 this.wifiPassword.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_WIFI_PASSWORD, value));
161 this.localTime.map(value -> result.put(ArgoClimaBindingConstants.PROPERTY_LOCAL_TIME, value));
162 return Collections.unmodifiableSortedMap(result);
169 private final ArgoClimaTranslationProvider i18nProvider;
170 private String commandString;
171 private DeviceProperties properties;
174 * C-tor (from command string and properties - either from remote server response or device-side poll intercept)
176 * @param commandString The device-side {@code HMI} string, carrying its updates and commands
177 * @param properties The parsed device-side properties
178 * @param i18nProvider Framework's translation provider
179 * @implNote Consider: rewrite to a factory instead of this
181 public DeviceStatus(String commandString, DeviceProperties properties, ArgoClimaTranslationProvider i18nProvider) {
182 this.commandString = commandString;
183 this.properties = properties;
184 this.i18nProvider = i18nProvider;
188 * C-tor (from just-received status response - live poll)
190 * @param commandString The command string received
191 * @param lastSeenDateTime The date/time when the request has been received
192 * @param i18nProvider Framework's translation provider
194 public DeviceStatus(String commandString, OffsetDateTime lastSeenDateTime,
195 ArgoClimaTranslationProvider i18nProvider) {
196 this(commandString, new DeviceProperties(lastSeenDateTime), i18nProvider);
200 * Retrieve the device {@code HMI} string, carrying its updates and commands
202 * @return The status/command string
204 public String getCommandString() {
205 return this.commandString;
209 * Retrieve device-side properties
211 * @return Device properties
213 public DeviceProperties getProperties() {
214 return this.properties;
218 * Throw exception if last update time is older than
219 * {@link ArgoClimaConfigurationRemote#LAST_SEEN_UNAVAILABILITY_THRESHOLD the threshold}
221 * @throws ArgoApiCommunicationException If status is stale
223 public void throwIfStatusIsStale() throws ArgoApiCommunicationException {
224 var delta = this.properties.getLastSeenDelta();
225 if (delta.toSeconds() > ArgoClimaConfigurationRemote.LAST_SEEN_UNAVAILABILITY_THRESHOLD.toSeconds()) {
226 throw new ArgoApiCommunicationException(
227 // "or more", since this message is also used in thing status (and we're not updating
228 // offline->offline). Actual "Last seen" can always be retrieved from properties
229 "Device was last seen {0} (or more) mins ago (threshold is set at {1} min). Please ensure the HVAC is connected to Wi-Fi and communicating with Argo servers",
230 "thing-status.cause.argoclima.remote-device-stale", i18nProvider, delta.toMinutes(),
231 ArgoClimaConfigurationRemote.LAST_SEEN_UNAVAILABILITY_THRESHOLD.toMinutes());