]> git.basschouten.com Git - openhab-addons.git/blob
611115b81dd88917b232d9b2c79f7ebd92d4484d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.anel.internal.state;
14
15 import java.util.Arrays;
16 import java.util.IllegalFormatException;
17 import java.util.LinkedList;
18 import java.util.List;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
21 import java.util.stream.Collectors;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.anel.internal.IAnelConstants;
26
27 /**
28  * Parser and data structure for the state of an Anel device.
29  * <p>
30  * Documentation in <a href="https://forum.anel.eu/viewtopic.php?f=16&t=207">Anel forum</a> (German).
31  *
32  * @author Patrick Koenemann - Initial contribution
33  */
34 @NonNullByDefault
35 public class AnelState {
36
37     /** Pattern for temp, e.g. 26.4°C or -1°F */
38     private static final Pattern PATTERN_TEMPERATURE = Pattern.compile("(\\-?\\d+(?:\\.\\d)?).[CF]");
39     /** Pattern for switch state: [name],[state: 1=on,0=off] */
40     private static final Pattern PATTERN_SWITCH_STATE = Pattern.compile("(.+),(0|1)");
41     /** Pattern for IO state: [name],[1=input,0=output],[state: 1=on,0=off] */
42     private static final Pattern PATTERN_IO_STATE = Pattern.compile("(.+),(0|1),(0|1)");
43
44     /** The raw status this state was created from. */
45     public final String status;
46
47     /** Device IP address; read-only. */
48     public final @Nullable String ip;
49     /** Device name; read-only. */
50     public final @Nullable String name;
51     /** Device mac address; read-only. */
52     public final @Nullable String mac;
53
54     /** Device relay names; read-only. */
55     public final String[] relayName = new String[8];
56     /** Device relay states; changeable. */
57     public final Boolean[] relayState = new Boolean[8];
58     /** Device relay locked status; read-only. */
59     public final Boolean[] relayLocked = new Boolean[8];
60
61     /** Device IO names; read-only. */
62     public final String[] ioName = new String[8];
63     /** Device IO states; changeable if they are configured as input. */
64     public final Boolean[] ioState = new Boolean[8];
65     /** Device IO input states (<code>true</code> means changeable); read-only. */
66     public final Boolean[] ioIsInput = new Boolean[8];
67
68     /** Device temperature (optional); read-only. */
69     public final @Nullable String temperature;
70
71     /** Sensor temperature, e.g. "20.61" (optional); read-only. */
72     public final @Nullable String sensorTemperature;
73     /** Sensor Humidity, e.g. "40.7" (optional); read-only. */
74     public final @Nullable String sensorHumidity;
75     /** Sensor Brightness, e.g. "7.0" (optional); read-only. */
76     public final @Nullable String sensorBrightness;
77
78     private static final AnelState INVALID_STATE = new AnelState();
79
80     public static AnelState of(@Nullable String status) {
81         if (status == null || status.isEmpty()) {
82             return INVALID_STATE;
83         }
84         return new AnelState(status);
85     }
86
87     private AnelState() {
88         status = "<invalid>";
89         ip = null;
90         name = null;
91         mac = null;
92         temperature = null;
93         sensorTemperature = null;
94         sensorHumidity = null;
95         sensorBrightness = null;
96     }
97
98     private AnelState(@Nullable String status) throws IllegalFormatException {
99         if (status == null || status.isEmpty()) {
100             throw new IllegalArgumentException("status must not be null or empty");
101         }
102         this.status = status;
103         final String[] segments = status.split(IAnelConstants.STATUS_SEPARATOR);
104         if (!segments[0].equals(IAnelConstants.STATUS_RESPONSE_PREFIX)) {
105             throw new IllegalArgumentException(
106                     "Data must start with '" + IAnelConstants.STATUS_RESPONSE_PREFIX + "' but it didn't: " + status);
107         }
108         if (segments.length < 16) {
109             throw new IllegalArgumentException("Data must have at least 16 segments but it didn't: " + status);
110         }
111         final List<String> issues = new LinkedList<>();
112
113         // name, host, mac
114         name = segments[1].trim();
115         ip = segments[2];
116         mac = segments[5];
117
118         // 8 switches / relays
119         Integer lockedSwitches;
120         try {
121             lockedSwitches = Integer.parseInt(segments[14]);
122         } catch (NumberFormatException e) {
123             throw new IllegalArgumentException(
124                     "Segment 15 (" + segments[14] + ") is expected to be a number but it's not: " + status);
125         }
126         for (int i = 0; i < 8; i++) {
127             final Matcher matcher = PATTERN_SWITCH_STATE.matcher(segments[6 + i]);
128             if (matcher.matches()) {
129                 relayName[i] = matcher.group(1);
130                 relayState[i] = "1".equals(matcher.group(2));
131             } else {
132                 issues.add("Unexpected format for switch " + i + ": '" + segments[6 + i]);
133                 relayName[i] = "";
134                 relayState[i] = false;
135             }
136             relayLocked[i] = (lockedSwitches & (1 << i)) > 0;
137         }
138
139         // 8 IO ports (devices with IO ports have >=24 segments)
140         if (segments.length >= 24) {
141             for (int i = 0; i < 8; i++) {
142                 final Matcher matcher = PATTERN_IO_STATE.matcher(segments[16 + i]);
143                 if (matcher.matches()) {
144                     ioName[i] = matcher.group(1);
145                     ioIsInput[i] = "1".equals(matcher.group(2));
146                     ioState[i] = "1".equals(matcher.group(3));
147                 } else {
148                     issues.add("Unexpected format for IO " + i + ": '" + segments[16 + i]);
149                     ioName[i] = "";
150                 }
151             }
152         }
153
154         // temperature
155         temperature = segments.length > 24 ? parseTemperature(segments[24], issues) : null;
156
157         if (segments.length > 34 && "p".equals(segments[27])) {
158             // optional sensor (if device supports it and firmware >= 6.1) after power management
159             if (segments.length > 38 && "s".equals(segments[35])) {
160                 sensorTemperature = segments[36];
161                 sensorHumidity = segments[37];
162                 sensorBrightness = segments[38];
163             } else {
164                 sensorTemperature = null;
165                 sensorHumidity = null;
166                 sensorBrightness = null;
167             }
168         } else if (segments.length > 31 && "n".equals(segments[27]) && "s".equals(segments[28])) {
169             // but sensor! (if device supports it and firmware >= 6.1)
170             sensorTemperature = segments[29];
171             sensorHumidity = segments[30];
172             sensorBrightness = segments[31];
173         } else {
174             // firmware <= 6.0 or unknown format; skip rest
175             sensorTemperature = null;
176             sensorBrightness = null;
177             sensorHumidity = null;
178         }
179
180         if (!issues.isEmpty()) {
181             throw new IllegalArgumentException(String.format("Anel status string contains %d issue%s: %s\n%s", //
182                     issues.size(), issues.size() == 1 ? "" : "s", status,
183                     issues.stream().collect(Collectors.joining("\n"))));
184         }
185     }
186
187     private static @Nullable String parseTemperature(String temp, List<String> issues) {
188         if (!temp.isEmpty()) {
189             final Matcher matcher = PATTERN_TEMPERATURE.matcher(temp);
190             if (matcher.matches()) {
191                 return matcher.group(1);
192             }
193             issues.add("Unexpected format for temperature: " + temp);
194         }
195         return null;
196     }
197
198     @Override
199     public String toString() {
200         return getClass().getSimpleName() + "[" + status + "]";
201     }
202
203     /* generated */
204     @Override
205     @SuppressWarnings("null")
206     public int hashCode() {
207         final int prime = 31;
208         int result = 1;
209         result = prime * result + ((ip == null) ? 0 : ip.hashCode());
210         result = prime * result + ((mac == null) ? 0 : mac.hashCode());
211         result = prime * result + ((name == null) ? 0 : name.hashCode());
212         result = prime * result + Arrays.hashCode(ioIsInput);
213         result = prime * result + Arrays.hashCode(ioName);
214         result = prime * result + Arrays.hashCode(ioState);
215         result = prime * result + Arrays.hashCode(relayLocked);
216         result = prime * result + Arrays.hashCode(relayName);
217         result = prime * result + Arrays.hashCode(relayState);
218         result = prime * result + ((temperature == null) ? 0 : temperature.hashCode());
219         result = prime * result + ((sensorBrightness == null) ? 0 : sensorBrightness.hashCode());
220         result = prime * result + ((sensorHumidity == null) ? 0 : sensorHumidity.hashCode());
221         result = prime * result + ((sensorTemperature == null) ? 0 : sensorTemperature.hashCode());
222         return result;
223     }
224
225     /* generated */
226     @Override
227     @SuppressWarnings("null")
228     public boolean equals(@Nullable Object obj) {
229         if (this == obj) {
230             return true;
231         }
232         if (obj == null) {
233             return false;
234         }
235         if (getClass() != obj.getClass()) {
236             return false;
237         }
238         AnelState other = (AnelState) obj;
239         if (ip == null) {
240             if (other.ip != null) {
241                 return false;
242             }
243         } else if (!ip.equals(other.ip)) {
244             return false;
245         }
246         if (!Arrays.equals(ioIsInput, other.ioIsInput)) {
247             return false;
248         }
249         if (!Arrays.equals(ioName, other.ioName)) {
250             return false;
251         }
252         if (!Arrays.equals(ioState, other.ioState)) {
253             return false;
254         }
255         if (mac == null) {
256             if (other.mac != null) {
257                 return false;
258             }
259         } else if (!mac.equals(other.mac)) {
260             return false;
261         }
262         if (name == null) {
263             if (other.name != null) {
264                 return false;
265             }
266         } else if (!name.equals(other.name)) {
267             return false;
268         }
269         if (sensorBrightness == null) {
270             if (other.sensorBrightness != null) {
271                 return false;
272             }
273         } else if (!sensorBrightness.equals(other.sensorBrightness)) {
274             return false;
275         }
276         if (sensorHumidity == null) {
277             if (other.sensorHumidity != null) {
278                 return false;
279             }
280         } else if (!sensorHumidity.equals(other.sensorHumidity)) {
281             return false;
282         }
283         if (sensorTemperature == null) {
284             if (other.sensorTemperature != null) {
285                 return false;
286             }
287         } else if (!sensorTemperature.equals(other.sensorTemperature)) {
288             return false;
289         }
290         if (!Arrays.equals(relayLocked, other.relayLocked)) {
291             return false;
292         }
293         if (!Arrays.equals(relayName, other.relayName)) {
294             return false;
295         }
296         if (!Arrays.equals(relayState, other.relayState)) {
297             return false;
298         }
299         if (temperature == null) {
300             if (other.temperature != null) {
301                 return false;
302             }
303         } else if (!temperature.equals(other.temperature)) {
304             return false;
305         }
306         return true;
307     }
308 }