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.configuration;
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;
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;
37 * The {@link ArgoClimaConfigurationBase} class contains fields mapping thing configuration parameters.
38 * Contains common configuration parameters (same for all supported device types).
40 * @author Mateusz Bronk - Initial contribution
43 public abstract class ArgoClimaConfigurationBase extends Configuration implements IScheduleConfigurationProvider {
48 public interface ConfigValueSupplier<T> {
49 public T get() throws ArgoConfigurationException;
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}
57 private int refreshInterval = 30; // in seconds
58 private String deviceCpuId = "";
59 private int oemServerPort = 80;
60 private String oemServerAddress = "31.14.128.210";
62 // Note this boilerplate is actually necessary as these values are injected by framework!
63 private Set<Weekday> schedule1DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
65 private String schedule1OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
67 private String schedule1OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_1)
69 private Set<Weekday> schedule2DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
71 private String schedule2OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
73 private String schedule2OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_2)
75 private Set<Weekday> schedule3DayOfWeek = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
77 private String schedule3OnTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
79 private String schedule3OffTime = ArgoClimaConfigProvider.getScheduleDefaults(ScheduleTimerType.SCHEDULE_3)
82 public boolean resetToFactoryDefaults = false;
87 private static final DateTimeFormatter SCHEDULE_ON_OFF_TIME_FORMATTER = DateTimeFormatter.ofPattern("H:mm[:ss]");
88 protected @Nullable ArgoClimaTranslationProvider i18nProvider;
91 * Initializes the configuration class post construction, injecting i18n provider for localized configuration
94 * @implNote This class requires default/parameterless c-tor for framework-side initialization (from file)
95 * @param i18nProvider Framework's translation provider
97 public void initialize(ArgoClimaTranslationProvider i18nProvider) {
98 this.i18nProvider = i18nProvider;
102 * Get the user-configured CPUID of the Argo device (used in matching to a concrete device in a stub mode)
104 * @return The configured CPUID (if provided by the user = not blank)
106 public Optional<String> getDeviceCpuId() {
107 return this.deviceCpuId.isBlank() ? Optional.<String> empty() : Optional.of(this.deviceCpuId);
111 * Get the refresh interval the device is polled with (in seconds)
113 * @return The interval value {@code 0} - to disable polling
115 public int getRefreshInterval() {
116 return this.refreshInterval;
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)
124 * <b>Mode-specific considerations</b>:
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>
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
136 * @return True if the Thing is allowed to communicate outwards on its own, False otherwise
138 public boolean useDirectConnection() {
139 return getRefreshInterval() > 0; // Uses virtual method overridden for local device!
143 * The OEM server's address, used to pass through the communications to (in REMOTE_API_PROXY) mode
145 * @return The vendor's server IP address
146 * @throws ArgoConfigurationException In case the IP cannot be found
148 public InetAddress getOemServerAddress() throws ArgoConfigurationException {
150 return Objects.requireNonNull(InetAddress.getByName(oemServerAddress));
151 } catch (UnknownHostException e) {
152 throw ArgoConfigurationException.forInvalidParamValue(
153 ArgoClimaBindingConstants.PARAMETER_OEM_SERVER_ADDRESS, oemServerAddress, i18nProvider, e);
158 * The OEM server's port, used to pass through the communications to (in REMOTE_API_PROXY) mode
160 * @return Vendor's server port. {@code -1} for no value
162 public int getOemServerPort() {
163 return this.oemServerPort;
167 * Converts "raw" {@code Set<Weekday>} into an {@code EnumSet<Weekday>}
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
182 private EnumSet<Weekday> canonizeWeekdaysAfterDeserialization(Set<Weekday> rawInput, String paramName)
183 throws ArgoConfigurationException {
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();
191 var result = EnumSet.noneOf(Weekday.class);
192 daysStr.map(ds -> Weekday.valueOf(ds.strip())).forEach(wd -> result.add(wd));
195 // UI/API configuration (nicely strong-typed already)
196 return EnumSet.copyOf(rawInput);
198 } catch (ClassCastException | IllegalArgumentException e) {
199 throw ArgoConfigurationException.forInvalidParamValue(paramName, rawInput.toString(), i18nProvider, e);
203 record ConfigParam<K> (K paramValue, String paramName) {
207 public EnumSet<Weekday> getScheduleDayOfWeek(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
208 ConfigParam<Set<Weekday>> configValue;
209 switch (scheduleType) {
211 configValue = new ConfigParam<>(schedule1DayOfWeek,
212 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(1));
215 configValue = new ConfigParam<>(schedule2DayOfWeek,
216 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(2));
219 configValue = new ConfigParam<>(schedule3DayOfWeek,
220 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_DAYS.formatted(3));
223 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
226 if (configValue.paramValue().isEmpty()) {
227 return ArgoClimaConfigProvider.getScheduleDefaults(scheduleType).weekdays();
229 return canonizeWeekdaysAfterDeserialization(configValue.paramValue(), configValue.paramName());
233 public LocalTime getScheduleOnTime(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
234 ConfigParam<String> configValue;
235 switch (scheduleType) {
237 configValue = new ConfigParam<>(schedule1OnTime,
238 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(1));
241 configValue = new ConfigParam<>(schedule2OnTime,
242 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(2));
245 configValue = new ConfigParam<>(schedule3OnTime,
246 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_ON_TIME.formatted(3));
249 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
253 return LocalTime.parse(configValue.paramValue(), SCHEDULE_ON_OFF_TIME_FORMATTER);
254 } catch (DateTimeParseException e) {
255 throw ArgoConfigurationException.forInvalidParamValue(configValue.paramName(), configValue.paramValue(),
261 public LocalTime getScheduleOffTime(ScheduleTimerType scheduleType) throws ArgoConfigurationException {
262 ConfigParam<String> configValue;
263 switch (scheduleType) {
265 configValue = new ConfigParam<>(schedule1OffTime,
266 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(1));
269 configValue = new ConfigParam<>(schedule2OffTime,
270 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(2));
273 configValue = new ConfigParam<>(schedule3OffTime,
274 ArgoClimaBindingConstants.PARAMETER_SCHEDULE_X_OFF_TIME.formatted(3));
277 throw new IllegalArgumentException("Invalid schedule timer: " + scheduleType.toString());
281 return LocalTime.parse(configValue.paramValue(), SCHEDULE_ON_OFF_TIME_FORMATTER);
282 } catch (DateTimeParseException e) {
283 throw ArgoConfigurationException.forInvalidParamValue(configValue.paramName(), configValue.paramValue(),
288 /////////////////////
290 /////////////////////
293 * Utility function for logging only. Gets a parsed value from the supplier function or, exceptionally the raw
294 * value. Swallows exceptions.
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
301 protected static <@NonNull T> String getOrDefault(ConfigValueSupplier<T> fn) {
303 return fn.get().toString();
304 } catch (ArgoConfigurationException e) {
305 return e.rawValue + "[raw]";
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);
327 * Return derived class'es extra configuration parameters (for a common {@link toString} implementation)
329 * @return Comma-separated list of configuration parameter=value pairs or empty String if derived class does not
332 protected abstract String getExtraFieldDescription();
335 * Validate derived configuration
337 * @throws ArgoConfigurationException - on validation failure
339 protected abstract void validateInternal() throws ArgoConfigurationException;
342 * Validate current config
344 * @return Error message if config is invalid. Empty string - otherwise
346 public final String validate() {
348 if (refreshInterval < 0) {
349 throw ArgoConfigurationException.forParamBelowMin(ArgoClimaBindingConstants.PARAMETER_REFRESH_INTERNAL,
350 oemServerPort, i18nProvider, 0);
353 if (oemServerPort < 0 || oemServerPort > 65535) {
354 throw ArgoConfigurationException.forParamOutOfRange(ArgoClimaBindingConstants.PARAMETER_OEM_SERVER_PORT,
355 oemServerPort, i18nProvider, 0, 65535);
358 // want the side-effect of these calls
359 getOemServerAddress();
361 getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_1);
362 getScheduleOnTime(ScheduleTimerType.SCHEDULE_1);
363 getScheduleOffTime(ScheduleTimerType.SCHEDULE_1);
365 getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_2);
366 getScheduleOnTime(ScheduleTimerType.SCHEDULE_2);
367 getScheduleOffTime(ScheduleTimerType.SCHEDULE_2);
369 getScheduleDayOfWeek(ScheduleTimerType.SCHEDULE_3);
370 getScheduleOnTime(ScheduleTimerType.SCHEDULE_3);
371 getScheduleOffTime(ScheduleTimerType.SCHEDULE_3);
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
382 .concat(cause.map(c -> "\n\t[" + c.getClass().getSimpleName() + "]").orElse(""));