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.shelly.internal.api1;
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
19 import java.util.List;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
25 import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
26 import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
27 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
28 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.unit.ImperialUnits;
31 import org.openhab.core.library.unit.SIUnits;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.types.State;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 1
40 * @author Markus Michels - Initial contribution
43 public class Shelly1CoIoTVersion1 extends Shelly1CoIoTProtocol implements Shelly1CoIoTInterface {
44 private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTVersion1.class);
46 public Shelly1CoIoTVersion1(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
47 Map<String, CoIotDescrSen> sensorMap) {
48 super(thingName, thingHandler, blkMap, sensorMap);
52 public int getVersion() {
53 return Shelly1CoapJSonDTO.COIOT_VERSION_1;
57 * Process CoIoT status update message. If a status update is received, but the device description has not been
58 * received yet a GET is send to query device description.
60 * @param devId device id included in the status packet
61 * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
62 * @param serial Serial for this request. If this the the same as last serial
63 * the update was already sent and processed so this one gets
67 public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
68 Map<String, State> updates, ShellyColorUtils col) {
69 // first check the base implementation
70 if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
71 // process by the base class
75 // Process status information and convert into channel updates
76 Integer rIndex = Integer.parseInt(sen.links) + 1;
77 String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
78 : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
79 switch (sen.type.toLowerCase()) {
80 case "t": // Temperature +
81 Double value = getDouble(s.value);
82 switch (sen.desc.toLowerCase()) {
83 case "temperature": // Sensor Temp
84 if (getString(getProfile().settings.temperatureUnits)
85 .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
86 value = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(s.value))
89 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
90 toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
92 case "temperature f": // Device Temp -> ignore (we use C only)
94 case "temperature c": // Device Temp in C
96 updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
97 toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
99 case "external temperature f": // Shelly 1/1PM external temp sensors
100 // ignore F, we use C only
102 case "external temperature c": // Shelly 1/1PM external temp sensors
103 case "external_temperature":
104 int idx = getExtTempId(sen.id);
106 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
107 toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
109 logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
114 logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
117 case "p": // Power/Watt
118 // 3EM uses 1-based meter IDs, other 0-based
119 String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
120 : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
121 updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
122 toQuantityType(s.value, DIGITS_WATT, Units.WATT));
123 updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
125 case "s" /* CatchAll */:
126 switch (sen.desc.toLowerCase()) {
129 thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
132 case "energy counter 0 [w-min]":
133 updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
134 toQuantityType(s.value, DIGITS_WATT, Units.WATT));
136 case "energy counter 1 [w-min]":
137 case "energy counter 2 [w-min]":
140 case "energy counter total [w-h]": // 3EM reports W/h
141 case "energy counter total [w-min]":
142 Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
143 updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
144 toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
147 updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
148 toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
151 updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
152 toQuantityType(getDouble(s.value), DIGITS_AMPERE, Units.AMPERE));
155 updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
158 // work around: Roller reports 101% instead max 100
159 double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
160 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
161 toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
162 updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
163 toQuantityType(pos, Units.PERCENT));
165 case "input event": // Shelly Button 1
166 handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
168 case "input event counter": // Shelly Button 1/ix3
169 handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
172 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
173 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
175 case "tilt": // DW with FW1.6.5+ //+
176 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
177 toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
179 case "vibration": // DW with FW1.6.5+
180 if (profile.isMotion) {
182 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
183 s.value == 1 ? OnOffType.ON : OnOffType.OFF);
184 } else if (s.value == 1) {
186 thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
187 EVENT_TYPE_VIBRATION);
190 case "temp": // Shelly Bulb
191 case "colortemperature": // Shelly Duo
192 updateChannel(updates,
193 profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
195 ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
197 case "sensor state": // Shelly Gas
198 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
200 case "alarm state": // Shelly Gas
201 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
202 getStringType(s.valueStr));
204 case "self-test state":// Shelly Gas
205 updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
206 getStringType(s.valueStr));
208 case "concentration":// Shelly Gas
209 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
210 toQuantityType(getDouble(s.value), DIGITS_NONE, Units.PARTS_PER_MILLION));
213 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
230 * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
231 * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
232 * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
233 * old mapping stays in to support older firmware releases.
235 * @param sen Sensor description received from device
236 * @return fixed Sensor description (sen)
239 public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
240 // Shelly1: reports null descr+type "Switch" -> map to S
241 // Shelly1PM: reports null descr+type "Overtemp" -> map to O
242 // Shelly1PM: reports null descr+type "W" -> add description
243 // Shelly1PM: reports temp senmsors without desc -> add description
244 // Shelly Dimmer: sensors are reported without descriptions -> map to S
245 // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
246 // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
247 // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
248 // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
249 // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
251 throw new IllegalArgumentException("sen should not be null!");
253 if (sen.desc == null) {
256 String desc = sen.desc.toLowerCase();
258 // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
259 // we break it up to Power with L:0, Power with L:1...
260 if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
261 String newDesc = substringBefore(sen.desc, "_");
262 String newLink = substringAfter(sen.desc, "_");
265 if (!blkMap.containsKey(sen.links)) {
266 // auto-insert a matching blk entry
267 CoIotDescrBlk blk = new CoIotDescrBlk();
268 CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
271 blk.desc = blk0.desc + "_" + blk.id;
272 blkMap.put(blk.id, blk);
277 switch (sen.type.toLowerCase()) {
278 case "w": // old devices/firmware releases use "W", new ones "P"
284 sen.desc = "Temperature C";
288 sen.desc = "Temperature F";
292 sen.desc = "Overtemp";
302 switch (sen.desc.toLowerCase()) {
303 case "motion": // fix acc to spec it's T=M
307 case "battery": // fix: type is B not H
309 sen.desc = "Battery";
313 sen.desc = "Overtemp";
321 case "e cnt 0 [w-min]": // 4 Pro
322 case "e cnt 1 [w-min]":
323 case "e cnt 2 [w-min]":
324 case "e cnt total [w-min]": // 4 Pro
325 sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
330 if (sen.desc.isEmpty()) {
331 switch (sen.type.toLowerCase()) {
336 sen.desc = "Temperature";
348 sen.desc = "Brightness";
355 case "temp": // Bulb: Color temperature
360 // it seems that Shelly tends to break their own spec: T is the description and D is no longer
361 // included -> map D to sen.T and set CatchAll for T
365 // Default: set no description
366 // (there are no T values defined in the CoIoT spec)