2 * Copyright (c) 2010-2023 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.anel.internal.state;
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;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.anel.internal.IAnelConstants;
28 * Parser and data structure for the state of an Anel device.
30 * Documentation in <a href="https://forum.anel.eu/viewtopic.php?f=16&t=207">Anel forum</a> (German).
32 * @author Patrick Koenemann - Initial contribution
35 public class AnelState {
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)");
44 /** The raw status this state was created from. */
45 public final String status;
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;
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];
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];
68 /** Device temperature (optional); read-only. */
69 public final @Nullable String temperature;
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;
78 private static final AnelState INVALID_STATE = new AnelState();
80 public static AnelState of(@Nullable String status) {
81 if (status == null || status.isEmpty()) {
84 return new AnelState(status);
93 sensorTemperature = null;
94 sensorHumidity = null;
95 sensorBrightness = null;
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");
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);
108 if (segments.length < 16) {
109 throw new IllegalArgumentException("Data must have at least 16 segments but it didn't: " + status);
111 final List<String> issues = new LinkedList<>();
114 name = segments[1].trim();
118 // 8 switches / relays
119 Integer lockedSwitches;
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);
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));
132 issues.add("Unexpected format for switch " + i + ": '" + segments[6 + i]);
134 relayState[i] = false;
136 relayLocked[i] = (lockedSwitches & (1 << i)) > 0;
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));
148 issues.add("Unexpected format for IO " + i + ": '" + segments[16 + i]);
155 temperature = segments.length > 24 ? parseTemperature(segments[24], issues) : null;
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];
164 sensorTemperature = null;
165 sensorHumidity = null;
166 sensorBrightness = null;
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];
174 // firmware <= 6.0 or unknown format; skip rest
175 sensorTemperature = null;
176 sensorBrightness = null;
177 sensorHumidity = null;
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"))));
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);
193 issues.add("Unexpected format for temperature: " + temp);
199 public String toString() {
200 return getClass().getSimpleName() + "[" + status + "]";
205 @SuppressWarnings("null")
206 public int hashCode() {
207 final int prime = 31;
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());
227 @SuppressWarnings("null")
228 public boolean equals(@Nullable Object obj) {
235 if (getClass() != obj.getClass()) {
238 AnelState other = (AnelState) obj;
240 if (other.ip != null) {
243 } else if (!ip.equals(other.ip)) {
246 if (!Arrays.equals(ioIsInput, other.ioIsInput)) {
249 if (!Arrays.equals(ioName, other.ioName)) {
252 if (!Arrays.equals(ioState, other.ioState)) {
256 if (other.mac != null) {
259 } else if (!mac.equals(other.mac)) {
263 if (other.name != null) {
266 } else if (!name.equals(other.name)) {
269 if (sensorBrightness == null) {
270 if (other.sensorBrightness != null) {
273 } else if (!sensorBrightness.equals(other.sensorBrightness)) {
276 if (sensorHumidity == null) {
277 if (other.sensorHumidity != null) {
280 } else if (!sensorHumidity.equals(other.sensorHumidity)) {
283 if (sensorTemperature == null) {
284 if (other.sensorTemperature != null) {
287 } else if (!sensorTemperature.equals(other.sensorTemperature)) {
290 if (!Arrays.equals(relayLocked, other.relayLocked)) {
293 if (!Arrays.equals(relayName, other.relayName)) {
296 if (!Arrays.equals(relayState, other.relayState)) {
299 if (temperature == null) {
300 if (other.temperature != null) {
303 } else if (!temperature.equals(other.temperature)) {