]> git.basschouten.com Git - openhab-addons.git/blob
73f0ffb3880817e3cb5f49a8b8cedf6b52ac61ed
[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.configuration;
14
15 import java.net.InetAddress;
16 import java.net.UnknownHostException;
17 import java.time.LocalTime;
18 import java.time.format.DateTimeFormatter;
19 import java.time.format.DateTimeParseException;
20 import java.util.EnumSet;
21 import java.util.Objects;
22 import java.util.Optional;
23 import java.util.Set;
24
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.argoclima.internal.ArgoClimaBindingConstants;
29 import org.openhab.binding.argoclima.internal.ArgoClimaConfigProvider;
30 import org.openhab.binding.argoclima.internal.ArgoClimaTranslationProvider;
31 import org.openhab.binding.argoclima.internal.device.api.types.Weekday;
32 import org.openhab.binding.argoclima.internal.exception.ArgoConfigurationException;
33 import org.openhab.binding.argoclima.internal.utils.StringUtils;
34 import org.openhab.core.config.core.Configuration;
35
36 /**
37  * The {@link ArgoClimaConfigurationBase} class contains fields mapping thing configuration parameters.
38  * Contains common configuration parameters (same for all supported device types).
39  *
40  * @author Mateusz Bronk - Initial contribution
41  */
42 @NonNullByDefault
43 public abstract class ArgoClimaConfigurationBase extends Configuration implements IScheduleConfigurationProvider {
44     /////////////////////
45     // TYPES
46     /////////////////////
47     @FunctionalInterface
48     public interface ConfigValueSupplier<T> {
49         public T get() throws ArgoConfigurationException;
50     }
51
52     /////////////////////
53     // Configuration parameters
54     // These names are defined in thing-types.xml and/or ArgoClimaConfigProvider and get injected on instantiation
55     // through {@link org.openhab.core.thing.binding.BaseThingHandler#getConfigAs getConfigAs}
56     /////////////////////
57     private int refreshInterval = 30; // in seconds
58     private String deviceCpuId = "";
59     private int oemServerPort = 80;
60     private String oemServerAddress = "31.14.128.210";
61
62     // Note this boilerplate is actually necessary as these values are injected by framework!
63     private Set<Weekday> schedule1DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
64             .weekdays();
65     private String schedule1OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
66             .startTime();
67     private String schedule1OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
68             .endTime();
69     private Set<Weekday> schedule2DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
70             .weekdays();
71     private String schedule2OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
72             .startTime();
73     private String schedule2OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
74             .endTime();
75     private Set<Weekday> schedule3DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
76             .weekdays();
77     private String schedule3OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
78             .startTime();
79     private String schedule3OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
80             .endTime();
81
82     public boolean resetToFactoryDefaults = false;
83
84     /////////////////////
85     // Other fields
86     /////////////////////
87     private static final DateTimeFormatter SCHEDULE_ON_OFF_TIME_FORMATTER = DateTimeFormatter.ofPattern("H:mm[:ss]");
88     protected @Nullable ArgoClimaTranslationProvider i18nProvider;
89
90     /**
91      * Initializes the configuration class post construction, injecting i18n provider for localized configuration
92      * exceptions
93      *
94      * @implNote This class requires default/parameterless c-tor for framework-side initialization (from file)
95      * @param i18nProvider Framework's translation provider
96      */
97     public void initialize(ArgoClimaTranslationProvider i18nProvider) {
98         this.i18nProvider = i18nProvider;
99     }
100
101     /**
102      * Get the user-configured CPUID of the Argo device (used in matching to a concrete device in a stub mode)
103      *
104      * @return The configured CPUID (if provided by the user = not blank)
105      */
106     public Optional<String> getDeviceCpuId() {
107         return this.deviceCpuId.isBlank() ? Optional.<String> empty() : Optional.of(this.deviceCpuId);
108     }
109
110     /**
111      * Get the refresh interval the device is polled with (in seconds)
112      *
113      * @return The interval value {@code 0} - to disable polling
114      */
115     public int getRefreshInterval() {
116         return this.refreshInterval;
117     }
118
119     /**
120      * If true, allows the binding to directly communicate with the device (or vendor's server - for remote thing type).
121      * When false, binding will not communicate directly with the device and wait for it to call it (through
122      * intercepting/stub server)
123      * <p>
124      * <b>Mode-specific considerations</b>:
125      * <ul>
126      * <li>in {@code REMOTE_API_STUB} mode - will not issue any outbound connections on its own</li>
127      * <li>in {@code REMOTE_API_PROXY} mode - will still communicate with vendor's servers but ONLY when queried by the
128      * device (a pass-through)</li>
129      * </ul>
130      *
131      * @implNote While this is configured by its dedicated settings (for better UX) and valid only for Local Thing
132      *           types, internal implementation uses {@code refreshInterval == 0} to signify no comms. This is because
133      *           without a refresh, the binding would have to function in a fire and forget mode sending commands back
134      *           to HVAC and never receiving any ACK... which makes little sense, hence is not supported
135      *
136      * @return True if the Thing is allowed to communicate outwards on its own, False otherwise
137      */
138     public boolean useDirectConnection() {
139         return getRefreshInterval() > 0; // Uses virtual method overridden for local device!
140     }
141
142     /**
143      * The OEM server's address, used to pass through the communications to (in REMOTE_API_PROXY) mode
144      *
145      * @return The vendor's server IP address
146      * @throws ArgoConfigurationException In case the IP cannot be found
147      */
148     public InetAddress getOemServerAddress() throws ArgoConfigurationException {
149         try {
150             return Objects.requireNonNull(InetAddress.getByName(oemServerAddress));
151         } catch (UnknownHostException e) {
152             throw ArgoConfigurationException.forInvalidParamValue(
153                     ArgoClimaBindingConstants.PARAMETER_OEM_SERVER_ADDRESS, oemServerAddress, i18nProvider, e);
154         }
155     }
156
157     /**
158      * The OEM server's port, used to pass through the communications to (in REMOTE_API_PROXY) mode
159      *
160      * @return Vendor's server port. {@code -1} for no value
161      */
162     public int getOemServerPort() {
163         return this.oemServerPort;
164     }
165
166     /**
167      * Converts "raw" {@code Set<Weekday>} into an {@code EnumSet<Weekday>}
168      *
169      * @implNote Because this configuration parameter is *dynamic* (and deliberately not defined in
170      *           {@code thing-types.xml}) when OH is loading a textual thing file, it does not have a full definition
171      *           yet, hence CANNOT infer its data type.
172      *           The Thing.xtext definition for {@code ModelProperty} allows for arrays, but these are always implicit/
173      *           For example {@code schedule1DayOfWeek="MON","TUE"} deserializes as a Collection (and is properly cast
174      *           to enum later), however a {@code schedule1DayOfWeek="MON"} deserializes to a String, and causes a
175      *           {@link ClassCastException} on access. This impl. accounts for that forced "as-String" interpretation on
176      *           load, and coerces such values back to a collection.
177      * @param rawInput The value to process
178      * @param paramName Name of the textual parameter (for error messaging)
179      * @return Converted value
180      * @throws ArgoConfigurationException In case the conversion fails
181      */
182     private EnumSet<Weekday> canonizeWeekdaysAfterDeserialization(Set<Weekday> rawInput, String paramName)
183             throws ArgoConfigurationException {
184         try {
185             var items = rawInput.toArray();
186             if (items.length == 1 && !(items[0] instanceof Weekday)) {
187                 // Text based configuration -> falling back to string parse
188                 var strValue = StringUtils.strip(items[0].toString(), "[]- \t\"'").trim();
189                 var daysStr = StringUtils.splitByWholeSeparator(strValue, ",").stream();
190
191                 var result = EnumSet.noneOf(Weekday.class);
192                 daysStr.map(ds -> Weekday.valueOf(ds.strip())).forEach(wd -> result.add(wd));
193                 return result;
194             } else {
195                 // UI/API configuration (nicely strong-typed already)
196                 return EnumSet.copyOf(rawInput);
197             }
198         } catch (ClassCastException | IllegalArgumentException e) {
199             throw ArgoConfigurationException.forInvalidParamValue(paramName, rawInput.toString(), i18nProvider, e);
200         }
201     }
202
203     record ConfigParam<K> (K paramValue, String paramName) {
204     }
205
206     @Override
207     public EnumSet<Weekday> getScheduleDayOfWeek(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
208         ConfigParam<Set<Weekday>> configValue;
209         switch (scheduleType) {
210             case SCHEDULE_1:
211                 configValue = new ConfigParam<>(schedule1DayOfWeek,
212                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(1));
213                 break;
214             case SCHEDULE_2:
215                 configValue = new ConfigParam<>(schedule2DayOfWeek,
216                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(2));
217                 break;
218             case SCHEDULE_3:
219                 configValue = new ConfigParam<>(schedule3DayOfWeek,
220                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(3));
221                 break;
222             default:
223                 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
224         }
225
226         if (configValue.paramValue().isEmpty()) {
227             return ArgoClimaConfigProvider.getScheduleDefaults(scheduleType).weekdays();
228         }
229         return canonizeWeekdaysAfterDeserialization(configValue.paramValue(), configValue.paramName());
230     }
231
232     @Override
233     public LocalTime getScheduleOnTime(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
234         ConfigParam<String> configValue;
235         switch (scheduleType) {
236             case SCHEDULE_1:
237                 configValue = new ConfigParam<>(schedule1OnTime,
238                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(1));
239                 break;
240             case SCHEDULE_2:
241                 configValue = new ConfigParam<>(schedule2OnTime,
242                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(2));
243                 break;
244             case SCHEDULE_3:
245                 configValue = new ConfigParam<>(schedule3OnTime,
246                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(3));
247                 break;
248             default:
249                 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
250         }
251
252         try {
253             return LocalTime.parse(configValue.paramValue(), SCHEDULE_ON_OFF_TIME_FORMATTER);
254         } catch (DateTimeParseException e) {
255             throw ArgoConfigurationException.forInvalidParamValue(configValue.paramName(), configValue.paramValue(),
256                     i18nProvider, e);
257         }
258     }
259
260     @Override
261     public LocalTime getScheduleOffTime(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
262         ConfigParam<String> configValue;
263         switch (scheduleType) {
264             case SCHEDULE_1:
265                 configValue = new ConfigParam<>(schedule1OffTime,
266                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(1));
267                 break;
268             case SCHEDULE_2:
269                 configValue = new ConfigParam<>(schedule2OffTime,
270                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(2));
271                 break;
272             case SCHEDULE_3:
273                 configValue = new ConfigParam<>(schedule3OffTime,
274                         ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(3));
275                 break;
276             default:
277                 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
278         }
279
280         try {
281             return LocalTime.parse(configValue.paramValue(), SCHEDULE_ON_OFF_TIME_FORMATTER);
282         } catch (DateTimeParseException e) {
283             throw ArgoConfigurationException.forInvalidParamValue(configValue.paramName(), configValue.paramValue(),
284                     i18nProvider, e);
285         }
286     }
287
288     /////////////////////
289     // Helper functions
290     /////////////////////
291
292     /**
293      * Utility function for logging only. Gets a parsed value from the supplier function or, exceptionally the raw
294      * value. Swallows exceptions.
295      *
296      * @param <T> Actual type of variable returned by the supplier (parsed)
297      * @param fn Parser function
298      * @return String param value (if parsed correctly), or the default value post-fixed with {@code [raw]} - on parse
299      *         failure.
300      */
301     protected static <@NonNull T> String getOrDefault(ConfigValueSupplier<T> fn) {
302         try {
303             return fn.get().toString();
304         } catch (ArgoConfigurationException e) {
305             return e.rawValue + "[raw]";
306         }
307     }
308
309     @Override
310     public final String toString() {
311         return String.format("Config: { %s, deviceCpuId=%s, refreshInterval=%d, oemServerPort=%d, oemServerAddress=%s,"
312                 + "schedule1DayOfWeek=%s, schedule1OnTime=%s, schedule1OffTime=%s, schedule2DayOfWeek=%s, schedule2OnTime=%s, schedule2OffTime=%s, schedule3DayOfWeek=%s, schedule3OnTime=%s, schedule3OffTime=%s, resetToFactoryDefaults=%s}",
313                 getExtraFieldDescription(), deviceCpuId, refreshInterval, oemServerPort,
314                 getOrDefault(this::getOemServerAddress),
315                 getOrDefault(() -> getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_1)),
316                 getOrDefault(() -> getScheduleOnTime(ScheduleTimerType.SCHEDULE_1)),
317                 getOrDefault(() -> getScheduleOffTime(ScheduleTimerType.SCHEDULE_1)),
318                 getOrDefault(() -> getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_2)),
319                 getOrDefault(() -> getScheduleOnTime(ScheduleTimerType.SCHEDULE_2)),
320                 getOrDefault(() -> getScheduleOffTime(ScheduleTimerType.SCHEDULE_2)),
321                 getOrDefault(() -> getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_3)),
322                 getOrDefault(() -> getScheduleOnTime(ScheduleTimerType.SCHEDULE_3)),
323                 getOrDefault(() -> getScheduleOffTime(ScheduleTimerType.SCHEDULE_3)), resetToFactoryDefaults);
324     }
325
326     /**
327      * Return derived class'es extra configuration parameters (for a common {@link toString} implementation)
328      *
329      * @return Comma-separated list of configuration parameter=value pairs or empty String if derived class does not
330      *         introduce any.
331      */
332     protected abstract String getExtraFieldDescription();
333
334     /**
335      * Validate derived configuration
336      *
337      * @throws ArgoConfigurationException - on validation failure
338      */
339     protected abstract void validateInternal() throws ArgoConfigurationException;
340
341     /**
342      * Validate current config
343      *
344      * @return Error message if config is invalid. Empty string - otherwise
345      */
346     public final String validate() {
347         try {
348             if (refreshInterval < 0) {
349                 throw ArgoConfigurationException.forParamBelowMin(ArgoClimaBindingConstants.PARAMETER_REFRESH_INTERNAL,
350                         oemServerPort, i18nProvider, 0);
351             }
352
353             if (oemServerPort < 0 || oemServerPort > 65535) {
354                 throw ArgoConfigurationException.forParamOutOfRange(ArgoClimaBindingConstants.PARAMETER_OEM_SERVER_PORT,
355                         oemServerPort, i18nProvider, 0, 65535);
356             }
357
358             // want the side-effect of these calls
359             getOemServerAddress();
360
361             getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_1);
362             getScheduleOnTime(ScheduleTimerType.SCHEDULE_1);
363             getScheduleOffTime(ScheduleTimerType.SCHEDULE_1);
364
365             getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_2);
366             getScheduleOnTime(ScheduleTimerType.SCHEDULE_2);
367             getScheduleOffTime(ScheduleTimerType.SCHEDULE_2);
368
369             getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_3);
370             getScheduleOnTime(ScheduleTimerType.SCHEDULE_3);
371             getScheduleOffTime(ScheduleTimerType.SCHEDULE_3);
372
373             validateInternal();
374             return "";
375         } catch (Exception e) {
376             var msg = Optional.ofNullable(e.getLocalizedMessage());
377             var cause = Optional.ofNullable(e.getCause());
378             return msg.orElse("Unknown exception, message is null") // The message theoretically can be null
379                                                                     // (Exception's i-face) but in practice never is, so
380                                                                     // keeping cryptic non-i18nized text instead of
381                                                                     // throwing
382                     .concat(cause.map(c -> "\n\t[" + c.getClass().getSimpleName() + "]").orElse(""));
383         }
384     }
385 }