]> git.basschouten.com Git - openhab-addons.git/blob
0d738ced246a2c71ab7f4869d58d45730c9f3ad1
[openhab-addons.git] /
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.device.api;
14
15 import java.net.URL;
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;
26
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;
36
37 /**
38  * Represents the current device status, as-communicated by the device (either push or pull model)
39  * <p>
40  * Includes both the "raw" {@link #getCommandString() commandString} as well as {@link #getProperties() properties}
41  *
42  * @author Mateusz Bronk - Initial contribution
43  */
44 @NonNullByDefault
45 class DeviceStatus {
46     //////////////
47     // TYPES
48     //////////////
49     /**
50      * Helper class for dealing with device properties
51      *
52      * @author Mateusz Bronk - Initial contribution
53      */
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();
67
68         /**
69          * C-tor (from remote server query response)
70          *
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
74          */
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");
79         }
80
81         /**
82          * C-tor (from live poll response)
83          *
84          * @param lastSeen The date/time of last update (when the response got received)
85          */
86         public DeviceProperties(OffsetDateTime lastSeen) {
87             this.localIP = Optional.empty();
88             this.lastSeen = Optional.of(lastSeen);
89             this.vendorUiUrl = Optional.empty();
90         }
91
92         /**
93          * C-tor (from intercepted device-side query to remote)
94          *
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)
97          */
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;
110         }
111
112         private static Optional<OffsetDateTime> dateFromISOString(String isoDateTime, String contextualName) {
113             if (isoDateTime.isEmpty()) {
114                 return Optional.empty();
115             }
116
117             try {
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,
122                         ex.getMessage());
123                 return Optional.empty();
124             }
125         }
126
127         private static String dateTimeToStringLocal(OffsetDateTime toConvert, TimeZoneProvider timeZoneProvider) {
128             var timeAtZone = toConvert.atZoneSameInstant(timeZoneProvider.getTimeZone());
129             return timeAtZone.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG));
130         }
131
132         /**
133          * Returns duration between last update and now. If last update is N/A, picking lowest possible time value
134          *
135          * @return Time elapsed since last device-side update
136          */
137         Duration getLastSeenDelta() {
138             return Duration.between(lastSeen.orElse(OffsetDateTime.MIN).toInstant(), Instant.now());
139         }
140
141         /**
142          * Return the properties in a map (ready to pass on to openHAB engine)
143          *
144          * @param timeZoneProvider TZ provider, for parsing date/time values
145          * @return Properties map
146          */
147         SortedMap<String, String> asPropertiesRaw(TimeZoneProvider timeZoneProvider) {
148             var result = new TreeMap<String, String>();
149
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);
163         }
164     }
165
166     //////////////
167     // FIELDS
168     //////////////
169     private final ArgoClimaTranslationProvider i18nProvider;
170     private String commandString;
171     private DeviceProperties properties;
172
173     /**
174      * C-tor (from command string and properties - either from remote server response or device-side poll intercept)
175      *
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
180      */
181     public DeviceStatus(String commandString, DeviceProperties properties, ArgoClimaTranslationProvider i18nProvider) {
182         this.commandString = commandString;
183         this.properties = properties;
184         this.i18nProvider = i18nProvider;
185     }
186
187     /**
188      * C-tor (from just-received status response - live poll)
189      *
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
193      */
194     public DeviceStatus(String commandString, OffsetDateTime lastSeenDateTime,
195             ArgoClimaTranslationProvider i18nProvider) {
196         this(commandString, new DeviceProperties(lastSeenDateTime), i18nProvider);
197     }
198
199     /**
200      * Retrieve the device {@code HMI} string, carrying its updates and commands
201      *
202      * @return The status/command string
203      */
204     public String getCommandString() {
205         return this.commandString;
206     }
207
208     /**
209      * Retrieve device-side properties
210      *
211      * @return Device properties
212      */
213     public DeviceProperties getProperties() {
214         return this.properties;
215     }
216
217     /**
218      * Throw exception if last update time is older than
219      * {@link ArgoClimaConfigurationRemote#LAST_SEEN_UNAVAILABILITY_THRESHOLD the threshold}
220      *
221      * @throws ArgoApiCommunicationException If status is stale
222      */
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());
232         }
233     }
234 }