## Supported Devices
+### Generation 1:
+
| thing-type | Model | Vendor ID |
|--------------------|--------------------------------------------------------|-----------|
| shelly1 | Shelly 1 Single Relay Switch | SHSW-1 |
`true`: Brightness will be set and device output is powered = light turns on with the new brightness
`false`: Brightness will be set, but output stays unchanged so light will not be switched on when it's currently off.
-### Shelly RGBW2 in White Mode (thing-type: shellyrgbw2-color)
+### Shelly RGBW2 in Color Mode (thing-type: shellyrgbw2-color)
|Group |Channel |Type |read-only|Description |
|----------|-------------|---------|---------|-----------------------------------------------------------------------|
|control |power |Switch |r/w |Switch light ON/OFF |
-| |input |Switch |yes |State of Input |
-| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF; in sec |
-| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON: in sec |
+| |autoOn |Number |r/w |Sets a timer to turn the device ON after every OFF command; in seconds|
+| |autoOff |Number |r/w |Sets a timer to turn the device OFF after every ON command; in seconds|
| |timerActive |Switch |yes |ON: An auto-on/off timer is active |
-|color | | | |Color settings: only valid in COLOR mode |
-| |hsb |HSB |r/w |Represents the color picker (HSBType), control r/g/b, but not white |
-|meter |currentWatts |Number |yes |Current power consumption in Watts (all channels) |
-
-Please note that the settings of channel group color are only valid in color mode and vice versa for white mode.
-The current firmware doesn't support the timestamp report for the meters.
-The binding emulates this by using the system time on every update.
+|color |hsb |HSB |r/w |Represents the color picker (HSBType), control r/g/b, bight not white |
+| |full |String |r/w |Set Red / Green / Blue / Yellow / White mode and switch mode |
+| | | |r/w |Valid settings: "red", "green", "blue", "yellow", "white" or "r,g,b,w" |
+| |red |Dimmer |r/w |Red brightness: 0..100% or 0..255 (control only the red channel) |
+| |green |Dimmer |r/w |Green brightness: 0..100% or 0..255 (control only the green channel) |
+| |blue |Dimmer |r/w |Blue brightness: 0..100% or 0..255 (control only the blue channel) |
+| |white |Dimmer |r/w |White brightness: 0..100% or 0..255 (control only the white channel) |
+| |gain |Dimmer |r/w |Gain setting: 0..100% or 0..100 |
+| |effect |Number |r/w |Puts the light into effect mode: 0..3) |
+| | | | |0=No effect, 1=Meteor Shower, 2=Gradual Change, 3=Flash |
+|meter |currentWatts |Number |yes |Current power consumption in Watts |
+| |lastPower1 |Number |yes |Energy consumption for a round minute, 1 minute ago |
+| |totalKWH |Number |yes |Total energy consumption in kWh since the device powered up (resets on restart)|
+| |lastUpdate |DateTime |yes |Timestamp of the last measurement |
### Shelly RGBW2 in White Mode (thing-type: shellyrgbw2-white)
*/
package org.openhab.binding.shelly.internal;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
public static final String BINDING_ID = "shelly";
public static final String SYSTEM_ID = "system";
- // Type names
- public static final String THING_TYPE_SHELLY1_STR = "shelly1";
- public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
- public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
- public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
- public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
- public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
- public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
- public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
- public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
- public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
- public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
- public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
- public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
- public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
- public static final String THING_TYPE_SHELLYPLUGU1_STR = "shellyplugu1"; // Shely Plug US
- public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
- public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
- public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
- public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
- public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
- public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
- public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
- public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-color";
- public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-white";
- public static final String THING_TYPE_SHELLYDUORGBW_STR = "shellycolorbulb";
- public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
- public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
- public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
- public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
- public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
- public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
- public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
- public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
- public static final String THING_TYPE_SHELLYTRV_STR = "shellytrv";
- public static final String THING_TYPE_SHELLYMOTION_STR = "shellymotion";
- public static final String THING_TYPE_SHELLYMOTION2_STR = "shellymotion2";
- public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
- public static final String THING_TYPE_SHELLYBUTTON2_STR = "shellybutton2";
- public static final String THING_TYPE_SHELLYUNI_STR = "shellyuni";
-
- // Shelly Plus Seriens
- public static final String THING_TYPE_SHELLYPLUS1_STR = "shellyplus1";
- public static final String THING_TYPE_SHELLYPLUS1PM_STR = "shellyplus1pm";
- public static final String THING_TYPE_SHELLYPLUS2PM_RELAY_STR = "shellyplus2pm-relay";
- public static final String THING_TYPE_SHELLYPLUS2PM_ROLLER_STR = "shellyplus2pm-roller";
- public static final String THING_TYPE_SHELLYPLUSI4_STR = "shellyplusi4";
- public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
- public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
-
- // Shelly Pro Series
- public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
- public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
- public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
- public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
- public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
- public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
- public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
-
- public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
- public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
-
- // Device Types
- public static final String SHELLYDT_1 = "SHSW-1";
- public static final String SHELLYDT_1PM = "SHSW-PM";
- public static final String SHELLYDT_1L = "SHSW-L";
- public static final String SHELLYDT_SHPLG = "SHPLG-1";
- public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
- public static final String SHELLYDT_SHPLG_U1 = "SHPLG-U1";
- public static final String SHELLYDT_SHELLY2 = "SHSW-21";
- public static final String SHELLYDT_SHELLY25 = "SHSW-25";
- public static final String SHELLYDT_SHPRO = "SHSW-44";
- public static final String SHELLYDT_EM = "SHEM";
- public static final String SHELLYDT_3EM = "SHEM-3";
- public static final String SHELLYDT_HT = "SHHT-1";
- public static final String SHELLYDT_DW = "SHDW-1";
- public static final String SHELLYDT_DW2 = "SHDW-2";
- public static final String SHELLYDT_SENSE = "SHSEN-1";
- public static final String SHELLYDT_MOTION = "SHMOS-01";
- public static final String SHELLYDT_MOTION2 = "SHMOS-02";
- public static final String SHELLYDT_GAS = "SHGS-1";
- public static final String SHELLYDT_DIMMER = "SHDM-1";
- public static final String SHELLYDT_DIMMER2 = "SHDM-2";
- public static final String SHELLYDT_IX3 = "SHIX3-1";
- public static final String SHELLYDT_BULB = "SHBLB-1";
- public static final String SHELLYDT_DUO = "SHBDUO-1";
- public static final String SHELLYDT_DUORGBW = "SHCB-1";
- public static final String SHELLYDT_VINTAGE = "SHVIN-1";
- public static final String SHELLYDT_RGBW2 = "SHRGBW2";
- public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
- public static final String SHELLYDT_BUTTON2 = "SHBTN-2";
- public static final String SHELLYDT_UNI = "SHUNI-1";
- public static final String SHELLYDT_TRV = "SHTRV-01";
-
- // Shelly Plus Series
- public static final String SHELLYDT_PLUS1 = "SNSW-001X16EU";
- public static final String SHELLYDT_PLUS1PM = "SNSW-001P16EU";
- public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
- public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
- public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
- public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
- public static final String SHELLYDT_PLUSHT = "SNSN-0013A";
-
- // Shelly Pro Series
- public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
- public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
- public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
- public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
- public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
- public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
- public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
-
- // List of all Thing Type UIDs
- public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY1L = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1L_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLY2_RELAY_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLY2_ROLLER_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLY25_RELAY_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLY25_ROLLER_STR);
- public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYPLUGU1 = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYPLUGU1_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYUNI = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYUNI_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYDIMMER_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYDIMMER2_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYVINTAGE_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDUORGBW = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYDUORGBW_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYDOORWIN_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYDOORWIN2_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYTRV = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYTRV_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYBUTTON1_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYBUTTON2 = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYBUTTON2_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
- public static final ThingTypeUID THING_TYPE_SHELLMOTION = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYMOTION_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYRGBW2_COLOR_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYRGBW2_WHITE_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYPROTECTED_STR);
- public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
- THING_TYPE_SHELLYUNKNOWN_STR);
-
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, THING_TYPE_SHELLY1PM,
THING_TYPE_SHELLYEM, THING_TYPE_SHELLY3EM, THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER,
*/
package org.openhab.binding.shelly.internal;
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
private final HttpClient httpClient;
private final ShellyTranslationProvider messages;
- private final ShellyCoapServer coapServer;
+ private final Shelly1CoapServer coapServer;
private final ShellyThingTable thingTable;
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
@Reference ShellyTranslationProvider translationProvider, @Reference ShellyThingTable thingTable,
@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
Map<String, Object> configProperties) {
- logger.debug("Activate Shelly HandlerFactory");
super.activate(componentContext);
messages = translationProvider;
// Save bindingConfig & pass it to all registered listeners
logger.debug("Using OH HTTP port {}", httpPort);
this.thingTable = thingTable;
- this.coapServer = new ShellyCoapServer();
-
- // Promote Shelly Manager usage
- logger.info("{}", messages.get("status.managerstarted", localIP, httpPort));
+ this.coapServer = new Shelly1CoapServer();
}
@Override
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
/**
public void setSleepTime(int value) throws ShellyApiException;
- public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException;
+ public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException;
public void setRelayTurn(int id, String turnMode) throws ShellyApiException;
- public ShellyControlRoller getRollerStatus(int rollerIndex) throws ShellyApiException;
+ public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException;
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.api;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
-import org.openhab.core.thing.CommonTriggerEvents;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * The {@link ShellyApiJsonDTO} is used for the JSon/GSon mapping
- *
- * @author Markus Michels - Initial contribution
- */
-public class ShellyApiJsonDTO {
- public static final String SHELLY_NULL_URL = "null";
- public static final String SHELLY_URL_DEVINFO = "/shelly";
- public static final String SHELLY_URL_STATUS = "/status";
- public static final String SHELLY_URL_SETTINGS = "/settings";
- public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap";
- public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta";
- public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta";
- public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
- public static final String SHELLY_URL_LIST_IR = "/ir/list";
- public static final String SHELLY_URL_SEND_IR = "/ir/emit";
- public static final String SHELLY_URL_RESTART = "/reboot";
-
- public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
- public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
- public static final String SHELLY_URL_CONTROL_RELEAY = "/relay";
-
- public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter";
- public static final String SHELLY_URL_STATUS_EMETER = "/emeter";
- public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv";
-
- public static final String SHELLY_URL_CONTROL_ROLLER = "/roller";
- public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller";
-
- public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light";
- public static final String SHELLY_URL_STATUS_LIGHT = "/light";
- public static final String SHELLY_URL_CONTROL_LIGHT = "/light";
-
- public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light";
-
- // Wakeup reasons
- public static final String SHELLY_WAKEUPT_SENSOR = "SENSOR"; // new sensordata
- public static final String SHELLY_WAKEUPT_PERIODIC = "PERIODIC"; // periodic wakeup
- public static final String SHELLY_WAKEUPT_BUTTON = "BUTTON"; // button pressed
- public static final String SHELLY_WAKEUPT_POWERON = "POWERON"; // device powered up
- public static final String SHELLY_WAKEUPT_EXT_POWER = "EXT_POWER"; // charger connected
- public static final String SHELLY_WAKEUPT_UNKNOWN = "UNKNOWN"; // other event
-
- //
- // Action URLs according to the device type
- //
- public static final String SHELLY_EVENTURL_SUFFIX = "_url";
-
- // Relay
- public static final String SHELLY_EVENT_BTN_ON = "btn_on";
- public static final String SHELLY_EVENT_BTN_OFF = "btn_off";
- public static final String SHELLY_EVENT_OUT_ON = "out_on";
- public static final String SHELLY_EVENT_OUT_OFF = "out_off";
- public static final String SHELLY_EVENT_SHORTPUSH = "shortpush";
- public static final String SHELLY_EVENT_LONGPUSH = "longpush";
- // Button
- public static final String SHELLY_EVENT_DOUBLE_SHORTPUSH = "double_shortpush";
- public static final String SHELLY_EVENT_TRIPLE_SHORTPUSH = "triple_shortpush";
- public static final String SHELLY_EVENT_SHORT_LONGTPUSH = "shortpush_longpush";
- public static final String SHELLY_EVENT_LONG_SHORTPUSH = "longpush_shortpush";
-
- // Dimmer
- public static final String SHELLY_EVENT_BTN1_ON = "btn1_on";
- public static final String SHELLY_EVENT_BTN1_OFF = "btn1_off";
- public static final String SHELLY_EVENT_BTN2_ON = "btn2_on";
- public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off";
- public static final String SHELLY_EVENT_SHORTPUSH1 = "btn1_shortpush";
- public static final String SHELLY_EVENT_LONGPUSH1 = "btn1_longpush";
- public static final String SHELLY_EVENT_SHORTPUSH2 = "btn2_shortpush";
- public static final String SHELLY_EVENT_LONGPUSH2 = "btn2_longpush";
-
- // Roller
- public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open";
- public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close";
- public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop";
- public static final String SHELLY_EVENT_ROLLER_CALIB = "roller_calibrating";
-
- // Roller states
- public static final String SHELLY_RSTATE_OPEN = "open";
- public static final String SHELLY_RSTATE_STOP = "stop";
- public static final String SHELLY_RSTATE_CLOSE = "close";
-
- // Sensors
- public static final String SHELLY_EVENT_SENSORREPORT = "report";
- public static final String SHELLY_EVENT_DARK = "dark";
- public static final String SHELLY_EVENT_TWILIGHT = "twilight";
- public static final String SHELLY_EVENT_BRIGHT = "bright";
- public static final String SHELLY_EVENT_FLOOD_DETECTED = "flood_detected";
- public static final String SHELLY_EVENT_FLOOD_GONE = "flood_gone";
- public static final String SHELLY_EVENT_VIBRATION = "vibration"; // DW 1.6.5+
- public static final String SHELLY_EVENT_OPEN = "open"; // DW 1.6.5+
- public static final String SHELLY_EVENT_CLOSE = "close"; // DW 1.6.5+
- public static final String SHELLY_EVENT_TEMP_OVER = "temp_over"; // FW 1.7
- public static final String SHELLY_EVENT_TEMP_UNDER = "temp_under"; // FW 1.7
-
- // Gas
- public static final String SHELLY_EVENT_ALARM_MILD = "alarm_mild"; // DW 1.7+
- public static final String SHELLY_EVENT_ALARM_HEAVY = "alarm_heavy"; // DW 1.7+
- public static final String SHELLY_EVENT_ALARM_OFF = "alarm_off"; // DW 1.7+
-
- //
- // API values
- //
- public static final double SHELLY_API_INVTEMP = -999.0;
-
- public static final String SHELLY_BTNT_MOMENTARY = "momentary";
- public static final String SHELLY_BTNT_MOM_ON_RELEASE = "momentary_on_release";
- public static final String SHELLY_BTNT_ONE_BUTTON = "one_button";
- public static final String SHELLY_BTNT_TWO_BUTTON = "dual_button";
- public static final String SHELLY_BTNT_TOGGLE = "toggle";
- public static final String SHELLY_BTNT_EDGE = "edge";
- public static final String SHELLY_BTNT_DETACHED = "detached";
-
- public static final String SHELLY_STATE_LAST = "last";
- public static final String SHELLY_STATE_STOP = "stop";
-
- public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose";
- public static final String SHELLY_INP_MODE_ONEBUTTON = "onebutton";
-
- public static final String SHELLY_OBSTMODE_DISABLED = "disabled";
- public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening";
-
- public static final String SHELLY_ALWD_TRIGGER_NONE = "none";
- public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open";
- public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close";
- public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop";
-
- // API Error Codes
- public static final String SHELLY_APIERR_UNAUTHORIZED = "Unauthorized";
- public static final String SHELLY_APIERR_TIMEOUT = "Timeout";
- public static final String SHELLY_APIERR_NOT_CALIBRATED = "Not calibrated!";
-
- // API device types / properties
- public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode
- public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode
- public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode
- public static final String SHELLY_CLASS_EMETER = "emeter"; // EM/EM3: emeter
-
- public static final String SHELLY_API_ON = "on";
- public static final String SHELLY_API_OFF = "off";
- public static final String SHELLY_API_TRUE = "true";
- public static final String SHELLY_API_FALSE = "false";
-
- public static final String SHELLY_API_MODE = "mode";
- public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode
- public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode
- public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode
- public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode
-
- public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable";
- public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable";
-
- public static final String SHELLY_API_STOPR_NORMAL = "normal";
- public static final String SHELLY_API_STOPR_SAFETYSW = "safety_switch";
- public static final String SHELLY_API_STOPR_OBSTACLE = "obstacle";
- public static final String SHELLY_API_STOPR_OVERPOWER = "overpower";
-
- public static final String SHELLY_TIMER_AUTOON = "auto_on";
- public static final String SHELLY_TIMER_AUTOOFF = "auto_off";
- public static final String SHELLY_TIMER_ACTIVE = "has_timer";
-
- public static final String SHELLY_LIGHT_TURN = "turn";
- public static final String SHELLY_LIGHT_DEFSTATE = "def_state";
- public static final String SHELLY_LIGHTTIMER = "timer";
-
- public static final String SHELLY_COLOR_RED = "red";
- public static final String SHELLY_COLOR_BLUE = "blue";
- public static final String SHELLY_COLOR_GREEN = "green";
- public static final String SHELLY_COLOR_YELLOW = "yellow";
- public static final String SHELLY_COLOR_WHITE = "white";
- public static final String SHELLY_COLOR_GAIN = "gain";
- public static final String SHELLY_COLOR_BRIGHTNESS = "brightness";
- public static final String SHELLY_COLOR_TEMP = "temp";
- public static final String SHELLY_COLOR_EFFECT = "effect";
-
- public static final int SHELLY_MIN_ROLLER_POS = 0;
- public static final int SHELLY_MAX_ROLLER_POS = 100;
- public static final int SHELLY_MIN_BRIGHTNESS = 0;
- public static final int SHELLY_MAX_BRIGHTNESS = 100;
- public static final int SHELLY_MIN_GAIN = 0;
- public static final int SHELLY_MAX_GAIN = 100;
- public static final int SHELLY_MIN_COLOR = 0;
- public static final int SHELLY_MAX_COLOR = 255;
- public static final int SHELLY_DIM_STEPSIZE = 10;
-
- // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100
- public static final int MIN_COLOR_TEMP_BULB = 3000;
- public static final int MAX_COLOR_TEMP_BULB = 6500;
- public static final int MIN_COLOR_TEMP_DUO = 2700;
- public static final int MAX_COLOR_TEMP_DUO = 6500;
- public static final int COLOR_TEMP_RANGE_BULB = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
- public static final int COLOR_TEMP_RANGE_DUO = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
- public static final double MIN_BRIGHTNESS = 0.0;
- public static final double MAX_BRIGHTNESS = 100.0;
- public static final double SATURATION_FACTOR = 2.55;
- public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100;
- public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100;
-
- // Door/Window
- public static final String SHELLY_API_ILLUM_DARK = "dark";
- public static final String SHELLY_API_ILLUM_TWILIGHT = "twilight";
- public static final String SHELLY_API_ILLUM_BRIGHT = "bright";
- public static final String SHELLY_API_DWSTATE_OPEN = "open";
- public static final String SHELLY_API_DWSTATE_CLOSE = "close";
-
- // Shelly Sense
- public static final String SHELLY_IR_CODET_STORED = "stored";
- public static final String SHELLY_IR_CODET_PRONTO = "pronto";
- public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex";
-
- // Bulb/Duo/RGBW2
- public static final int SHELLY_MIN_EFFECT = 0;
- public static final int SHELLY_MAX_EFFECT = 6;
-
- // Button
- public static final String SHELLY_BTNEVENT_1SHORTPUSH = "S";
- public static final String SHELLY_BTNEVENT_2SHORTPUSH = "SS";
- public static final String SHELLY_BTNEVENT_3SHORTPUSH = "SSS";
- public static final String SHELLY_BTNEVENT_LONGPUSH = "L";
- public static final String SHELLY_BTNEVENT_SHORTLONGPUSH = "SL";
- public static final String SHELLY_BTNEVENT_LONGSHORTPUSH = "LS";
-
- public static final String SHELLY_TEMP_CELSIUS = "C";
- public static final String SHELLY_TEMP_FAHRENHEIT = "F";
-
- // Motion
- public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset
-
- // TRV
- public static final int SHELLY_TRV_MIN_TEMP = 5; // < 5: means: lowest (valve fully closed)
- public static final int SHELLY_TRV_MAX_TEMP = 30; // > 30: means: highest (valve fully open)
-
- public static final String SHELLY_TRV_MODE_MANUAL = "manual";
- public static final String SHELLY_TRV_MODE_AUTO = "automatic";
-
- // CoIoT Multicast setting
- public static final String SHELLY_COIOT_MCAST = "mcast";
-
- public static class ShellySettingsDevice {
- public String type;
- public String mac;
- public String hostname;
- public String fw;
- public Boolean auth;
-
- @SerializedName("coiot") // Shelly Motion Multicast Endpoint
- public String coiot;
- public Integer longid;
-
- @SerializedName("num_outputs")
- public Integer numOutputs;
- @SerializedName("num_meters")
- public Integer numMeters;
- @SerializedName("num_emeters")
- public Integer numEMeters;
- @SerializedName("num_rollers")
- public Integer numRollers;
- }
-
- public static class ShellySettingsWiFiAp {
- public Boolean enabled;
- public String ssid;
- public String key;
- }
-
- public static class ShellySettingsWiFiNetwork {
- public Boolean enabled;
- public String ssid;
- public Integer rssi;
-
- @SerializedName("ipv4_method")
- public String ipv4Method;
- public String ip;
- public String gw;
- public String mask;
- public String dns;
- }
-
- public static class ShellySettingsMqtt {
- public Boolean enable;
- public String server;
- public String user;
- @SerializedName("reconnect_timeout_max")
- public Double reconnectTimeoutMax;
- @SerializedName("reconnect_timeout_min")
- public Double reconnectTimeoutMin;
- @SerializedName("clean_session")
- public Boolean cleanSession;
- @SerializedName("keep_alive")
- public Integer keepAlive;
- @SerializedName("will_topic")
- public String willTopic;
- @SerializedName("will_message")
- public String willMessage;
- @SerializedName("max_qos")
- public Integer maxQOS;
- public Boolean retain;
- @SerializedName("update_period")
- public Integer updatePeriod;
- }
-
- public static class ShellySettingsCoiot { // FW 1.6+
- @SerializedName("update_period")
- public Integer updatePeriod;
- public Boolean enabled; // Motion 1.0.7: Coap can be disabled
- public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast
- }
-
- public static class ShellyStatusMqtt {
- public Boolean connected;
- }
-
- public static class ShellySettingsSntp {
- public String server;
- public Boolean enabled;
- }
-
- public static class ShellySettingsLogin {
- public Boolean enabled;
- public Boolean unprotected;
- public String username;
- public String password;
- }
-
- public static class ShellySettingsBuildInfo {
- @SerializedName("build_id")
- public String buildId;
- @SerializedName("build_timestamp")
- public String buildTimestamp;
- @SerializedName("build_version")
- public String buildVersion;
- }
-
- public static class ShellyStatusCloud {
- public Boolean enabled;
- public Boolean connected;
- }
-
- public static class ShellySettingsHwInfo {
- @SerializedName("hw_revision")
- public String hwRevision;
- @SerializedName("batch_id")
- public Integer batchId;
- }
-
- public static class ShellySettingsScheduleRules {
- }
-
- public static class ShellySettingsRelay {
- public String name;
- @SerializedName("default_state")
- public String defaultState; // Accepted values: off, on, last, switch
- @SerializedName("btn_type")
- public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("btn1_type") // Shelly 1L
- public String btnType1;
- @SerializedName("btn2_type") // Shelly 1L
- public String btnType2;
- @SerializedName("has_timer")
- public Boolean hasTimer; // Whether a timer is currently armed for this channel
- @SerializedName("auto_on")
- public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
- @SerializedName("auto_off")
- public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
- @SerializedName("btn_on_url")
- public String btnOnUrl; // input is activated
- @SerializedName("btnOffUrl")
- public String btnOffUrl; // input is deactivated
- @SerializedName("out_on_url")
- public String outOnUrl; // output is activated
- @SerializedName("out_off_url")
- public String outOffUrl; // output is deactivated
- @SerializedName("roller_open_url")
- public String rollerOpenUrl; // to access when roller reaches open position
- @SerializedName("roller_close_url")
- public String rollerCloseUrl; // to access when roller reaches close position
- @SerializedName("roller_stop_url")
- public String rollerStopUrl; // to access when roller stopped
- @SerializedName("longpush_url")
- public String pushLongUrl; // to access when roller stopped
- @SerializedName("shortpush_url")
- public String pushShortUrl; // to access when roller stopped
-
- // Status information
- public Boolean ison;
- public Boolean overpower;
- @SerializedName("is_valid")
- public Boolean isValid;
- @SerializedName("ext_temperature")
- public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
- @SerializedName("ext_humidity")
- public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
- }
-
- public static class ShellySettingsDimmer {
- public String name; // unique name of the device
- public Boolean ison; // true: output is ON
- @SerializedName("default_state")
- public String defaultState; // Accepted values: off, on, last, switch
- @SerializedName("auto_on")
- public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
- @SerializedName("auto_off")
- public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
- @SerializedName("btn1_on_url")
- public String btn1OnUrl; // URL to access when SW input is activated
- @SerializedName("btn1_off_url")
- public String btn1OffUrl; // URL to access when SW input is deactivated
- @SerializedName("btn2_on_url")
- public String btn2OnUrl; // URL to access when SW input is activated
- @SerializedName("btn2_off_url")
- public String btn2OoffUrl; // URL to access when SW input is deactivated
- @SerializedName("out_on_url")
- public String outOnUrl; // URL to access when output is activated
- @SerializedName("out_off_url")
- public String outOffUrl; // URL to access when output is deactivated
- @SerializedName("longpush_url")
- public String pushLongUrl; // long push button event
- @SerializedName("shortpush_url")
- public String pushShortUrl; // short push button event
- @SerializedName("btn_type")
- public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("btn1_type")
- public String btnType1; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("btn2_type")
- public String btnType2; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("swap_inputs")
- public Integer swapInputs; // 0=no
- }
-
- public static class ShellySettingsRoller {
- public Double maxtime;
- @SerializedName("maxtime_open")
- public Double maxtimeOpen;
- @SerializedName("maxtime_close")
- public Double maxtimeClose;
- @SerializedName("default_state")
- public String defaultState; // see SHELLY_STATE_xxx
- public Boolean swap;
- @SerializedName("swap_inputs")
- public Boolean swapInputs;
- @SerializedName("input_mode")
- public String inputMode; // see SHELLY_INP_MODE_OPENCLOSE
- @SerializedName("button_type")
- public String buttonType; // // see SHELLY_BTNT_xxx
- @SerializedName("btn_Reverse")
- public Integer btnReverse;
- public String state;
- public Double power;
- @SerializedName("is_valid")
- public Boolean isValid;
- @SerializedName("safety_switch")
- public Boolean safetySwitch;
- @SerializedName("obstacle_mode")
- public String obstaclMode; // SHELLY_OBSTMODE_
- @SerializedName("obstacle_action")
- public String obstacleAction; // see SHELLY_STATE_xxx
- @SerializedName("obstacle_power")
- public Integer obstaclePower;
- @SerializedName("obstacle_delay")
- public Integer obstacleDelay;
- @SerializedName("safety_mode")
- public String safetyMode; // see SHELLY_SAFETYM_xxx
- @SerializedName("safety_action")
- public String safetyAction; // see SHELLY_STATE_xxx
- @SerializedName("safety_allowed_on_trigger")
- public String safetyAllowedOnTrigger; // see SHELLY_ALWD_TRIGGER_xxx
- @SerializedName("off_power")
- public Integer offPower;
- public Boolean positioning;
- }
-
- public static class ShellySettingsRgbwLight {
- public String name;
- public Boolean ison; // true: output is ON
- public Integer brightness;
- public Integer transition;
- @SerializedName("default_state")
- public String defaultState;
- @SerializedName("auto_on")
- public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
- @SerializedName("auto_off")
- public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
- public Boolean schedule;
- @SerializedName("btn_type")
- public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("btn_reverse")
- public Integer btnReverse; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
- @SerializedName("out_on_url")
- public String outOnUrl; // output is activated
- @SerializedName("out_off_url")
- public String outOffUrl; // output is deactivated
- }
-
- public static class ShellyFavPos { // FW 1.9.2+ in roller mode
- public String name;
- public Integer pos;
- }
-
- public static class ShellyInputState {
- public Integer input;
-
- // Shelly Button
- public String event;
- @SerializedName("event_cnt")
- public Integer eventCount;
- }
-
- public static class ShellySettingsMeter {
- @SerializedName("is_valid")
- public Boolean isValid;
- public Double power;
- public Double[] counters = { 0.0, 0.0, 0.0 };
- public Double total;
- public Long timestamp;
- }
-
- public static class ShellySettingsEMeter { // ShellyEM meter
- @SerializedName("is_valid")
- public Boolean isValid; // Whether the associated meter is functioning properly
- public Double power; // Instantaneous power, Watts
- public Double reactive; // Instantaneous reactive power, Watts
- public Double voltage; // RMS voltage, Volts
- public Double total; // Total consumed energy, Wh
- @SerializedName("total_returned")
- public Double totalReturned; // Total returned energy, Wh
-
- public Double pf; // 3EM
- public Double current; // 3EM
- }
-
- public static class ShellySettingsUpdate {
- public String status;
- @SerializedName("has_update")
- public Boolean hasUpdate;
- @SerializedName("new_version")
- public String newVersion;
- @SerializedName("old_version")
- public String oldVersion;
- @SerializedName("beta_version")
- public String betaVersion;
- }
-
- public static class ShellySettingsGlobal {
- // https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
- public ShellySettingsDevice device;
- @SerializedName("wifi_ap")
- public ShellySettingsWiFiAp wifiAp;
- @SerializedName("wifi_sta")
- public ShellySettingsWiFiNetwork wifiSta;
- @SerializedName("wifi_sta1")
- public ShellySettingsWiFiNetwork wifiSta1;
- @SerializedName("wifirecovery_reboot_enabled")
- public Boolean wifiRecoveryReboot; // FW 1.10+
- @SerializedName("ap_roaming")
- public ShellyApRoaming apRoaming; // FW 1.10+
-
- public ShellySettingsMqtt mqtt; // not used for now
- public ShellySettingsSntp sntp; // not used for now
- public ShellySettingsCoiot coiot; // Firmware 1.6+
- public ShellySettingsLogin login;
- @SerializedName("pin_code")
- public String pinCode;
- @SerializedName("coiot_execute_enable")
- public Boolean coiotExecuteEnable;
- public String name;
- public Boolean discoverable; // FW 1.6+
- public String fw;
- @SerializedName("build_info")
- public ShellySettingsBuildInfo buildInfo;
- public ShellyStatusCloud cloud;
- @SerializedName("sleep_mode")
- public ShellySensorSleepMode sleepMode; // FW 1.6
- @SerializedName("external_power")
- public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
- @SerializedName("debug_enable") // FW 1.10+
- public Boolean debugEnable;
-
- public String timezone;
- public Double lat;
- public Double lng;
- public Boolean tzautodetect;
- public String time;
-
- public ShellySettingsHwInfo hwinfo;
- public String mode;
- @SerializedName("max_power")
- public Double maxPower;
- public Boolean calibrated;
-
- public ArrayList<ShellySettingsRelay> relays;
- public ArrayList<ShellySettingsRoller> rollers;
- public ArrayList<ShellySettingsDimmer> dimmers;
- public ArrayList<ShellySettingsRgbwLight> lights;
- public ArrayList<ShellySettingsEMeter> emeters;
- public ArrayList<ShellySettingsInput> inputs; // ix3
- public ArrayList<ShellyThermnostat> thermostats; // TRV
-
- public Double voltage; // AC voltage for Shelly 2.5
- @SerializedName("supply_voltage")
- public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
-
- @SerializedName("temperature_units")
- public String temperatureUnits = "C"; // Either'C'or'F'
-
- @SerializedName("led_status_disable")
- public Boolean ledStatusDisable; // PlugS only Disable LED indication for network
- // status
- @SerializedName("led_power_disable")
- public Boolean ledPowerDisable; // PlugS only Disable LED indication for network
- // status
- @SerializedName("light_sensor")
- public String lightSensor; // Sense: sensor type
- @SerializedName("rain_sensor")
- public Boolean rainSensor; // Flood: true=in rain mode
-
- // FW 1.5.7: Door Window
- @SerializedName("dark_treshold")
- public Integer darkTreshold; // Illumination definition for "dark" in lux
- @SerializedName("twilight_treshold")
- public Integer twiLightTreshold; // Illumination definition for "twilight" in lux
- @SerializedName("dark_url")
- public String darkUrl; // URL to report to when luminance <= dark_threshold
- @SerializedName("twilight_url")
- public String twiLightUrl; // URL reports when luminance > dark_threshold AND luminance <=
- @SerializedName("close_url")
- public String closeUrl; // URL reports when DW contact is closed FW 1.6.5+
- @SerializedName("vibration_url")
- public String vibrationUrl; // URL reports when DW detects vibration FW 1.6.5+
-
- // Gas FW 1.7
- @SerializedName("set_volume")
- public Integer volume; // Speaker volume for alarm
- @SerializedName("alarm_off_url")
- public String alarmOffUrl; // URL reports when alarm went off
- @SerializedName("alarm_mild_url")
- public String alarmMidUrl; // URL reports middle alarm
- @SerializedName("alarm_heavy_url")
- public String alarmHeavyfUrl; // URL reports heavy alarm
-
- // Roller with FW 1.9.2+
- @SerializedName("favorites_enabled")
- public Boolean favoritesEnabled = false;
- public ArrayList<ShellyFavPos> favorites;
-
- // Motion
- public ShellyMotionSettings motion;
- @SerializedName("tamper_sensitivity")
- public Integer tamperSensitivity;
- @SerializedName("dark_threshold")
- public Integer darkThreshold;
- @SerializedName("twilight_threshold")
- public Integer twilightThreshold;
-
- @SerializedName("sleep_time") // Shelly Motion
- public Integer sleepTime;
- }
-
- public static class ShellySettingsAttributes {
- @SerializedName("device_type")
- public String deviceType; // Device model identifier
- @SerializedName("device_mac")
- public String deviceMac; // MAC address of the device in hexadecimal
- @SerializedName("wifi_ap")
- public String wifiAp; // WiFi access poInteger configuration, see /settings/ap for details
- @SerializedName("wifi_sta")
- public String wifiSta; // WiFi client configuration. See /settings/sta for details
- public String login; // credentials used for HTTP Basic authentication for the REST interface. If
- // enabled is true clients must include an Authorization: Basic ... HTTP header with valid
- // credentials when performing TP requests.
- public String name; // unique name of the device.
- public String fw; // current FW version
- }
-
- public static class ShellyActionsStats {
- public Integer skipped;
- }
-
- public static class ShellySettingsStatus {
- public String name; // FW 1.8: Symbolic Device name is configurable
-
- @SerializedName("wifi_sta")
- public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
- public ShellyStatusCloud cloud;
- public ShellyStatusMqtt mqtt;
-
- public String time;
- public Integer serial = -1;
- @SerializedName("has_update")
- public Boolean hasUpdate;
- public String mac;
- public Boolean discoverable; // FW 1.6+
- @SerializedName("cfg_changed_cnt")
- public Integer cfgChangedCount; // FW 1.8
- @SerializedName("actions_stats")
- public ShellyActionsStats astats;
-
- public ArrayList<ShellySettingsRelay> relays;
- public Double voltage; // Shelly 2.5
-
- public ArrayList<ShellySettingsRoller> rollers;
- public Integer input; // RGBW2 has no JSON array
- public ArrayList<ShellyInputState> inputs;
- public ArrayList<ShellySettingsLight> lights;
- public ArrayList<ShellyShortLightStatus> dimmers;
- public ArrayList<ShellySettingsMeter> meters;
- public ArrayList<ShellySettingsEMeter> emeters;
-
- // Internal device temp
- public ShellyStatusSensor.ShellySensorTmp tmp; // Shelly 1PM
- public Double temperature = SHELLY_API_INVTEMP; // Shelly 2.5
- public Boolean overtemperature;
-
- // Shelly Dimmer only
- public Boolean loaderror;
- public Boolean overload;
-
- // Shelly TRV
- public Boolean calibrated;
- public ArrayList<ShellyThermnostat> thermostats;
-
- public ShellySettingsUpdate update;
- @SerializedName("ram_total")
- public Long ramTotal;
- @SerializedName("ram_free")
- public Long ramFree;
- @SerializedName("fs_size")
- public Long fsSize;
- @SerializedName("fs_free")
- public Long fsFree;
- public Long uptime;
-
- @SerializedName("sleep_time") // Shelly Motion
- public Integer sleepTime;
-
- public String json;
- }
-
- public static class ShellySettingsInput {
- @SerializedName("btn_type")
- public String btnType;
- }
-
- public static class ShellyControlRelay {
- // https://shelly-api-docs.shelly.cloud/#shelly1-1pm-settings-relay-0
- @SerializedName("is_valid")
- public Boolean isValid;
- @SerializedName("has_timer")
- public Boolean hasTimer; // Whether a timer is currently armed for this channel
- @SerializedName("timer_remaining")
- public Integer timerRemaining; // FW 1.6+
- public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
-
- public String turn; // Accepted values are on and off. This will turn ON/OFF the respective output
- // channel when request is sent .
- public Integer timer; // A one-shot flip-back timer in seconds.
- }
-
- public static class ShellyShortStatusRelay {
- public String name; // FW 1.8+: Channel could now have a logical name
- @SerializedName("is_valid")
- public Boolean isValid;
- public Boolean ison; // Whether output channel is on or off
- @SerializedName("has_timer")
- public Boolean hasTimer; // Whether a timer is currently armed for this channel
- @SerializedName("timer_remaining")
- public Integer timerRemaining;
- public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
- public Double temperature; // Internal device temperature
- public Boolean overtemperature; // Device over heated
- }
-
- public static class ShellyShortLightStatus {
- public Boolean ison; // Whether output channel is on or off
- public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
- public Integer brightness; // brightness: 0.100%
-
- @SerializedName("has_timer")
- public Boolean hasTimer;
- }
-
- public static class ShellyStatusRelay {
- public String name; // FW 1.8: Symbolic channel name is configurable
-
- @SerializedName("wifi_sta")
- public ShellySettingsWiFiNetwork wifiSta; // WiFi status
- public ShellySettingsCoiot coiot; // Firmware 1.6+
- public Integer serial;
- public String mac; // MAC
- public ArrayList<ShellyShortStatusRelay> relays; // relay status
- public ArrayList<ShellySettingsMeter> meters; // current meter value
- public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
-
- @SerializedName("ext_temperature")
- public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
- @SerializedName("ext_humidity")
- public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
-
- public Double temperature; // device temp acc. on the selected temp unit
- public ShellyStatusSensor.ShellySensorTmp tmp;
- }
-
- public static class ShellyStatusDimmer {
- @SerializedName("wifi_sta")
- public ShellySettingsWiFiNetwork wifiSta; // WiFi status
- public ArrayList<ShellyShortLightStatus> lights; // relay status
- public ArrayList<ShellySettingsMeter> meters; // current meter value
-
- public ShellyStatusSensor.ShellySensorTmp tmp;
- public Boolean overtemperature;
-
- public Boolean loaderror;
- public Boolean overload;
- }
-
- public static class ShellyControlRoller {
- public String name; // FW 1.8: Symbolic name is configurable
-
- @SerializedName("roller_pos")
- public Integer rollerPos; // number Desired position in percent
- public Integer duration; // If specified, the motor will move for this period in seconds. If missing, the
- // value of maxtime in /settings/roller/N will be used.
- public String state; // One of stop, open, close
- public Double power; // Current power consumption in Watts
- @SerializedName("is_valid")
- public Boolean isValid; // If the power meter functions properly
- @SerializedName("safety_switch")
- public Boolean safetySwitch; // Whether the safety input is currently triggered
- public Boolean overtemperature;
- @SerializedName("stop_reason")
- public String stopReason; // Last cause for stopping: normal, safety_switch, obstacle
- @SerializedName("last_direction")
- public String lastDirection; // Last direction of motion, open or close
- public Boolean calibrating;
- public Boolean positioning; // true when calibration was performed
- @SerializedName("current_pos")
- public Integer currentPos; // current position 0..100, 100=open
- }
-
- public static class ShellyOtaCheckResult {
- public String status;
- }
-
- public static class ShellyApRoaming {
- public Boolean enabled;
- public Integer threshold;
- }
-
- public static class ShellySensorSleepMode {
- public Integer period;
- public String unit;
- }
-
- // Shelly TRV
- public class ShellyThermnostat {
- public class ShellyThermTargetTemp {
- public Boolean enabled;
- public Double value;
- public String unit;
- }
-
- public class ShellyThermTemp {
- public Double value;
- public String units;
- @SerializedName("is_valid")
- public Boolean isValid;
- }
-
- public Double pos;
- @SerializedName("target_t")
- public ShellyThermTargetTemp targetTemp;
- public Boolean schedule;
- @SerializedName("schedule_profile")
- public Integer profile;
- @SerializedName("schedule_profile_names")
- public String[] profileNames;
- public ShellyThermTemp tmp;
- @SerializedName("boost_minutes")
- public Integer boostMinutes;
- }
-
- public static class ShellyStatusSensor {
- // https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
- public static class ShellySensorTmp {
- public Double value; // Temperature in configured unites
- public String units; // 'C' or 'F'
- public Double tC; // temperature in deg C
- public Double tF; // temperature in deg F
- @SerializedName("is_valid")
- public Boolean isValid; // whether the internal sensor is operating properly
- }
-
- public static class ShellySensorHum {
- public Double value; // relative humidity in %
- }
-
- public static class ShellySensorBat {
- public Double value; // estimated remaining battery capacity in %
- public Double voltage; // battery voltage
- };
-
- // Door/Window sensor
- public static class ShellySensorState {
- @SerializedName("is_valid")
- public Boolean isValid; // whether the internal sensor is operating properly
- public String state; // Shelly Door/Window
-
- // Shelly Motion
- public Boolean motion;
- public Boolean vibration;
- @SerializedName("timestamp")
- public Long motionTimestamp;
- @SerializedName("active")
- public Boolean motionActive;
- }
-
- public static class ShellySensorLux {
- @SerializedName("is_valid")
- public Boolean isValid; // whether the internal sensor is operating properly
- public Double value;
-
- public String illumination;
- }
-
- public static class ShellySensorAccel {
- public Integer tilt; // Tilt in °
- public Integer vibration; // Whether vibration is detected
- }
-
- public static class ShellyMotionSettings {
- public Integer sensitivity;
- @SerializedName("blind_time_minutes")
- public Integer blindTimeMinutes;
- @SerializedName("pulse_count")
- public Integer pulseCount;
- @SerializedName("operating_mode")
- public Integer operatingMode;
- public Boolean enabled;
- }
-
- public static class ShellyExtTemperature {
- public static class ShellyShortTemp {
- public Double tC; // temperature in deg C
- public Double tF; // temperature in deg F
- }
-
- // Shelly 1/1PM have up to 3 sensors
- // for whatever reasons it's not an array, but 3 independent elements
- @SerializedName("0")
- public ShellyShortTemp sensor1;
- @SerializedName("1")
- public ShellyShortTemp sensor2;
- @SerializedName("2")
- public ShellyShortTemp sensor3;
- }
-
- public static class ShellyExtHumidity {
- public static class ShellyShortHum {
- public Double hum; // Humidity reading of sensor 0, percent
- }
-
- // Shelly 1/1PM have up to 3 sensors
- // for whatever reasons it's not an array, but 3 independent elements
- @SerializedName("0")
- public ShellyShortHum sensor1;
- }
-
- public static class ShellyADC {
- public Double voltage;
- }
-
- public ShellySensorTmp tmp;
- public ShellySensorHum hum;
- public ShellySensorLux lux;
- public ShellySensorAccel accel;
- public ShellySensorBat bat;
- @SerializedName("sensor")
- public ShellySensorState sensor;
- public Boolean smoke; // SHelly Smoke
- public Boolean flood; // Shelly Flood: true = flood condition detected
- @SerializedName("rain_sensor")
- public Boolean rainSensor; // Shelly Flood: true=in rain mode
-
- public Boolean motion; // Shelly Sense: true=motion detected
- public Boolean charger; // Shelly Sense, TRV: true=charger connected
-
- @SerializedName("act_reasons")
- public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
-
- @SerializedName("sensor_error")
- public String sensorError; // 1.5.7: Only displayed in case of error
-
- // FW 1.7: Shelly Gas
- @SerializedName("gas_sensor")
- public ShellyStatusGasSensor gasSensor;
- @SerializedName("concentration")
- public ShellyStatusGasConcentration concentration;
- public ArrayList<ShellyStatusValve> valves;
-
- // FW 1.7 Button
- @SerializedName("connect_retries")
- public Integer connectRetries;
- public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
-
- // Shelly UNI FW 1.9+
- public ArrayList<ShellyADC> adcs;
-
- // Shelly TRV
- public Boolean calibrated;
- public ArrayList<ShellyThermnostat> thermostats;
- }
-
- public static class ShellySettingsSmoke {
- @SerializedName("temperature_units")
- public String temperatureUnits; // Either 'C' or 'F'
- @SerializedName("temperature_threshold")
- public Integer temperatureThreshold; // Temperature delta (in configured degree units) which triggers an update
- @SerializedName("sleep_mode_period")
- public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24
- }
-
- // Shelly Gas
- // "gas_sensor":{"sensor_state":"normal","self_test_state":"not_completed","alarm_state":"none"},
- // "concentration":{"ppm":0,"is_valid":true},
- public static class ShellyStatusGasSensor {
- @SerializedName("sensor_state")
- public String sensorState;
- @SerializedName("self_test_state")
- public String selfTestState;
- @SerializedName("alarm_state")
- public String alarmState;
- }
-
- public static class ShellyStatusGasConcentration {
- public Integer ppm;
- @SerializedName("is_valid")
- public Boolean isValid;
- }
-
- public static class ShellyStatusValve {
- public String state; // closed/opened/not_connected/failure/closing/opening/checking
- }
-
- public static class ShellySettingsLight {
- public Integer red; // red brightness, 0..255, applies in mode="color"
- public Integer green; // green brightness, 0..255, applies in mode="color"
- public Integer blue; // blue brightness, 0..255, applies in mode="color"
- public Integer white; // white brightness, 0..255, applies in mode="color"
- public Integer gain; // gain for all channels, 0..100, applies in mode="color"
- public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
- public Integer brightness; // brightness, 0..100, applies in mode="white"
- public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
- // Change, 3: Breath,
- // 4: Flash, 5: On/Off Gradual, 6: Red/Green Change
- @SerializedName("default_state")
- public String defaultState; // one of on, off or last
- @SerializedName("auto_on")
- public Double autoOn; // see above
- @SerializedName("auto_off")
- public Double autoOff; // see above
-
- public Integer dcpower; // RGW2:Set to true for 24 V power supply, false for 12 V
-
- // Shelly Dimmer
- public String mode;
- public Boolean ison;
- }
-
- public static class ShellySettingsNightMode { // FW1.5.7+
- public Integer enabled;
- @SerializedName("start_time")
- public String startTime;
- @SerializedName("end_time")
- public String endTime;
- public Integer brightness;
- }
-
- public static class ShellyStatusLightChannel {
- public Boolean ison;
- public Double power;
- public Boolean overpower;
- @SerializedName("has_timer")
- public Boolean hasTimer;
- @SerializedName("timer_started")
- public Integer timerStarted;
- @SerializedName("timer_duration")
- public Integer timerDuration;
- @SerializedName("timer_remaining")
- public Integer timerRemaining;
-
- public Integer red; // red brightness, 0..255, applies in mode="color"
- public Integer green; // green brightness, 0..255, applies in mode="color"
- public Integer blue; // blue brightness, 0..255, applies in mode="color"
- public Integer white; // white brightness, 0..255, applies in mode="color"
- public Integer gain; // gain for all channels, 0..100, applies in mode="color"
- public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
- public Integer brightness; // brightness, 0..100, applies in mode="white"
- public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
- // Change, 3: Breath,
- }
-
- public static class ShellyStatusLight {
- public Boolean ison; // Whether output channel is on or off
- public Integer input;
-
- public ArrayList<ShellyStatusLightChannel> lights;
- public ArrayList<ShellySettingsMeter> meters;
- }
-
- public static class ShellySenseKeyCode {
- String id; // ID of the stored IR code into Shelly Sense.
- String name; // Short description or name of the stored IR code.
- }
-
- public static class ShellySendKeyList {
- @SerializedName("key_codes")
- public ArrayList<ShellySenseKeyCode> keyCodes;
- }
-
- /**
- * Shelly Dimmer returns light[]. However, the structure doesn't match the lights[] of a Bulb/RGBW2.
- * The tag lights[] will be replaced with dimmers[] so this could be mapped to a different Gson structure.
- * The function requires that it's only called when the device is a dimmer - on get settings and get status
- *
- * @param json Input Json as received by the API
- * @return Modified Json
- */
- public static String fixDimmerJson(String json) {
- return !json.contains("\"lights\":[") ? json
- : json.replaceFirst(java.util.regex.Pattern.quote("\"lights\":["), "\"dimmers\":[");
- }
-
- /**
- * Convert Shelly Button events into OH button states
- *
- * @param eventType S/SS/SSS or L
- * @return OH button states
- */
- public static String mapButtonEvent(String eventType) {
- // decode different codings
- // 0..2: CoAP
- // S/SS/SSS/L: CoAP for Button and xi3
- // shortpush/double_shortpush/triple_shortpush/longpush: REST
- switch (eventType) {
- case "0":
- return CommonTriggerEvents.RELEASED;
- case "1":
- case SHELLY_BTNEVENT_1SHORTPUSH:
- case SHELLY_EVENT_SHORTPUSH:
- return CommonTriggerEvents.SHORT_PRESSED;
- case SHELLY_BTNEVENT_2SHORTPUSH:
- case SHELLY_EVENT_DOUBLE_SHORTPUSH:
- return CommonTriggerEvents.DOUBLE_PRESSED;
- case SHELLY_BTNEVENT_3SHORTPUSH:
- case SHELLY_EVENT_TRIPLE_SHORTPUSH:
- return "TRIPLE_PRESSED";
- case "2":
- case SHELLY_BTNEVENT_LONGPUSH:
- case SHELLY_EVENT_LONGPUSH:
- return CommonTriggerEvents.LONG_PRESSED;
- case SHELLY_BTNEVENT_SHORTLONGPUSH:
- case SHELLY_EVENT_SHORT_LONGTPUSH:
- return "SHORT_LONG_PRESSED";
- case SHELLY_BTNEVENT_LONGSHORTPUSH:
- case SHELLY_EVENT_LONG_SHORTPUSH:
- return "LONG_SHORT_PRESSED";
- default:
- return "";
- }
- }
-}
package org.openhab.binding.shelly.internal.api;
import static org.eclipse.jetty.http.HttpStatus.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
public String response = "";
public int httpCode = -1;
public String httpReason = "";
+ public String authResponse = "";
public ShellyApiResult() {
}
package org.openhab.binding.shelly.internal.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public boolean discoverable = true;
public boolean auth = false;
public boolean alwaysOn = true;
+ public boolean isGen2 = false;
public String hwRev = "";
public String hwBatchId = "";
public boolean isHT = false; // true for H&T
public boolean isDW = false; // true for Door Window sensor
public boolean isButton = false; // true for a Shelly Button 1
- public boolean isIX3 = false; // true for a Shelly IX
+ public boolean isIX = false; // true for a Shelly IX
public boolean isTRV = false; // true for a Shelly TRV
public int minTemp = 0; // Bulb/Duo: Min Light Temp
boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
- isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
+ isHT = thingType.equals(THING_TYPE_SHELLYHT_STR) || thingType.equals(THING_TYPE_SHELLYPLUSHT_STR);
isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
- isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
+ isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR)
+ || thingType.equals(THING_TYPE_SHELLYPLUSI4DC_STR);
isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR);
isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
int idx = i + 1; // group names are 1-based
if (isRGBW2) {
return CHANNEL_GROUP_LIGHT_CONTROL;
- } else if (isIX3) {
+ } else if (isIX) {
return CHANNEL_GROUP_STATUS + idx;
} else if (isButton) {
return CHANNEL_GROUP_STATUS;
public String getInputSuffix(int i) {
int idx = i + 1; // channel names are 1-based
- if (isRGBW2 || isIX3) {
+ if (isRGBW2 || isIX) {
return ""; // RGBW2 has only 1 channel
} else if (isRoller || isDimmer) {
// Roller has 2 relays, but it will be mapped to 1 roller with 2 inputs
String btnType = "";
if (isButton) {
return true;
- } else if (isIX3 && settings.inputs != null && idx < settings.inputs.size()) {
+ } else if (isIX && settings.inputs != null && idx < settings.inputs.size()) {
ShellySettingsInput input = settings.inputs.get(idx);
btnType = getString(input.btnType);
} else if (isDimmer) {
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.api;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
-import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
-import org.openhab.core.library.unit.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not
- * cloud api).
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyHttpApi implements ShellyApiInterface {
- public static final String HTTP_HEADER_AUTH = "Authorization";
- public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
- public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
-
- private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class);
- private final HttpClient httpClient;
- private ShellyThingConfiguration config = new ShellyThingConfiguration();
- private String thingName;
- private final Gson gson = new Gson();
- private int timeoutErrors = 0;
- private int timeoutsRecovered = 0;
-
- private ShellyDeviceProfile profile = new ShellyDeviceProfile();
-
- public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
- this.httpClient = httpClient;
- this.thingName = thingName;
- setConfig(thingName, config);
- profile.initFromThingType(thingName);
- }
-
- @Override
- public void setConfig(String thingName, ShellyThingConfiguration config) {
- this.thingName = thingName;
- this.config = config;
- }
-
- @Override
- public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
- return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
- }
-
- @Override
- public String setDebug(boolean enabled) throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
- }
-
- @Override
- public String getDebugLog(String id) throws ShellyApiException {
- return callApi("/debug/" + id, String.class);
- }
-
- /**
- * Initialize the device profile
- *
- * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
- * @return Initialized ShellyDeviceProfile
- * @throws ShellyApiException
- */
- @Override
- public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
- String json = request(SHELLY_URL_SETTINGS);
- if (json.contains("\"type\":\"SHDM-")) {
- logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
- json = fixDimmerJson(json);
- }
-
- // Map settings to device profile for Light and Sense
- profile.initialize(thingType, json);
-
- // 2nd level initialization
- profile.thingName = profile.hostname;
- if (profile.isLight && (profile.numMeters == 0)) {
- logger.debug("{}: Get number of meters from light status", thingName);
- ShellyStatusLight status = getLightStatus();
- profile.numMeters = status.meters != null ? status.meters.size() : 0;
- }
- if (profile.isSense) {
- profile.irCodes = getIRCodeList();
- logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
- }
-
- return profile;
- }
-
- @Override
- public boolean isInitialized() {
- return profile.initialized;
- }
-
- /**
- * Get generic device settings/status. Json returned from API will be mapped to a Gson object
- *
- * @return Device settings/status as ShellySettingsStatus object
- * @throws ShellyApiException
- */
- @Override
- public ShellySettingsStatus getStatus() throws ShellyApiException {
- String json = "";
- try {
- json = request(SHELLY_URL_STATUS);
- // Dimmer2 returns invalid json type for loaderror :-(
- json = getString(json.replace("\"loaderror\":0,", "\"loaderror\":false,"));
- json = getString(json.replace("\"loaderror\":1,", "\"loaderror\":true,"));
- ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
- status.json = json;
- return status;
- } catch (JsonSyntaxException e) {
- throw new ShellyApiException("Unable to parse JSON: " + json, e);
- }
- }
-
- @Override
- public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException {
- return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class);
- }
-
- @Override
- public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
- callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
- ShellyShortLightStatus.class);
- }
-
- @Override
- public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
- return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
- ShellyShortLightStatus.class);
- }
-
- @Override
- public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
- String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
- request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness);
- }
-
- @Override
- public ShellyControlRoller getRollerStatus(int idx) throws ShellyApiException {
- String uri = SHELLY_URL_CONTROL_ROLLER + "/" + idx + "/pos";
- return callApi(uri, ShellyControlRoller.class);
- }
-
- @Override
- public void setRollerTurn(int idx, String turnMode) throws ShellyApiException {
- request(SHELLY_URL_CONTROL_ROLLER + "/" + idx + "?go=" + turnMode);
- }
-
- @Override
- public void setRollerPos(int id, int position) throws ShellyApiException {
- request(SHELLY_URL_CONTROL_ROLLER + "/" + id + "?go=to_pos&roller_pos=" + position);
- }
-
- public void setRollerTimer(int idx, int timer) throws ShellyApiException {
- request(SHELLY_URL_CONTROL_ROLLER + "/" + idx + "?timer=" + timer);
- }
-
- @Override
- public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
- return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
- }
-
- @Override
- public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
- ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
- if (profile.isSense) {
- // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
- status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
- : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
- .doubleValue();
- double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
- .convert(getDouble(status.tmp.value));
- status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
- }
- if ((status.charger == null) && (profile.settings.externalPower != null)) {
- // SHelly H&T uses external_power, Sense uses charger
- status.charger = profile.settings.externalPower != 0;
- }
- return status;
- }
-
- @Override
- public void setTimer(int index, String timerName, int value) throws ShellyApiException {
- String type = SHELLY_CLASS_RELAY;
- if (profile.isRoller) {
- type = SHELLY_CLASS_ROLLER;
- } else if (profile.isLight) {
- type = SHELLY_CLASS_LIGHT;
- }
- String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
- request(uri);
- }
-
- @Override
- public void setSleepTime(int value) throws ShellyApiException {
- request(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
- }
-
- @Override
- public void setValveTemperature(int valveId, int value) throws ShellyApiException {
- request("/thermostat/" + valveId + "?target_t_enabled=1&target_t=" + value);
- }
-
- @Override
- public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
- String uri = "/settings/thermostat/" + valveId + "?target_t_enabled=" + (auto ? "1" : "0");
- if (auto) {
- uri = uri + "&target_t=" + getDouble(profile.settings.thermostats.get(0).targetTemp.value);
- }
- request(uri); // percentage to open the valve
- }
-
- @Override
- public void setValveProfile(int valveId, int value) throws ShellyApiException {
- String uri = "/settings/thermostat/" + valveId + "?";
- request(uri + (value == 0 ? "schedule=0" : "schedule=1&schedule_profile=" + value));
- }
-
- @Override
- public void setValvePosition(int valveId, double value) throws ShellyApiException {
- request("/thermostat/" + valveId + "?pos=" + value); // percentage to open the valve
- }
-
- @Override
- public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
- request("/settings/thermostat/" + valveId + "?boost_minutes=" + value);
- }
-
- @Override
- public void startValveBoost(int valveId, int value) throws ShellyApiException {
- int minutes = value != -1 ? value : getInteger(profile.settings.thermostats.get(0).boostMinutes);
- request("/thermostat/" + valveId + "?boost_minutes=" + minutes);
- }
-
- @Override
- public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
- request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
- }
-
- public ShellySettingsLight getLightSettings() throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
- }
-
- @Override
- public ShellyStatusLight getLightStatus() throws ShellyApiException {
- return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
- }
-
- public void setLightSetting(String parm, String value) throws ShellyApiException {
- request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
- }
-
- @Override
- public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
- }
-
- @Override
- public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + urlEncode(user) + "&password="
- + urlEncode(password), ShellySettingsLogin.class);
- }
-
- @Override
- public String getCoIoTDescription() throws ShellyApiException {
- try {
- return callApi("/cit/d", String.class);
- } catch (ShellyApiException e) {
- if (e.getApiResult().isNotFound()) {
- return ""; // only supported by FW 1.10+
- }
- throw e;
- }
- }
-
- @Override
- public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
- }
-
- @Override
- public String deviceReboot() throws ShellyApiException {
- return callApi(SHELLY_URL_RESTART, String.class);
- }
-
- @Override
- public String factoryReset() throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
- }
-
- @Override
- public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
- return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
- }
-
- @Override
- public String setWiFiRecovery(boolean enable) throws ShellyApiException {
- return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
- String.class); // FW 1.10+: Enable auto-restart on WiFi problems
- }
-
- @Override
- public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
- return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
- }
-
- @Override
- public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
- return callApi("/sta_cache_reset", String.class);
- }
-
- public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
- return callApi("/ota?" + uri, ShellySettingsUpdate.class);
- }
-
- @Override
- public String setCloud(boolean enabled) throws ShellyApiException {
- return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
- }
-
- /**
- * Change between White and Color Mode
- *
- * @param mode
- * @throws ShellyApiException
- */
- @Override
- public void setLightMode(String mode) throws ShellyApiException {
- if (!mode.isEmpty() && !profile.mode.equals(mode)) {
- setLightSetting(SHELLY_API_MODE, mode);
- profile.mode = mode;
- profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
- }
- }
-
- /**
- * Set a single light parameter
- *
- * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
- * @param parm Name of the parameter (see API spec)
- * @param value The value
- * @throws ShellyApiException
- */
- @Override
- public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
- // Bulb, RGW2: /<color mode>/<light id>?parm?value
- // Dimmer: /light/<light id>?parm=value
- request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
- }
-
- @Override
- public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
- String url = getControlUriPrefix(lightIndex) + "?";
- int i = 0;
- for (String key : parameters.keySet()) {
- if (i > 0) {
- url = url + "&";
- }
- url = url + key + "=" + parameters.get(key);
- i++;
- }
- request(url);
- }
-
- /**
- * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
- * symbolic key code, which gets
- * map into a PRONTO code
- *
- * @return Map of key codes
- * @throws ShellyApiException
- */
- public Map<String, String> getIRCodeList() throws ShellyApiException {
- String result = request(SHELLY_URL_LIST_IR);
- // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
- String keyList = substringAfter(result, "[");
- keyList = substringBeforeLast(keyList, "]");
- keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
- keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
- keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
- String json = "{\"key_codes\" : [" + keyList + "] }";
- ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
- Map<String, String> list = new HashMap<>();
- for (ShellySenseKeyCode key : codes.keyCodes) {
- if (key != null) {
- list.put(key.id, key.name);
- }
- }
- return list;
- }
-
- /**
- * Sends a IR key code to the Shelly Sense.
- *
- * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
- * plain or hex64 format
- *
- * @throws ShellyApiException
- * @throws IllegalArgumentException
- */
- @Override
- public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
- String type = "";
- if (profile.irCodes.containsKey(keyCode)) {
- type = SHELLY_IR_CODET_STORED;
- } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
- type = SHELLY_IR_CODET_PRONTO;
- } else {
- type = SHELLY_IR_CODET_PRONTO_HEX;
- }
- String url = SHELLY_URL_SEND_IR + "?type=" + type;
- if (type.equals(SHELLY_IR_CODET_STORED)) {
- url = url + "&" + "id=" + keyCode;
- } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
- String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
- url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
- } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
- url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
- }
- request(url);
- }
-
- public void setSenseSetting(String setting, String value) throws ShellyApiException {
- request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
- }
-
- /**
- * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
- * redirected to the binding's servlet and act as a trigger to schedule a status update
- *
- * @param ShellyApiException
- * @throws ShellyApiException
- */
- @Override
- public void setActionURLs() throws ShellyApiException {
- setRelayEvents();
- setDimmerEvents();
- setSensorEventUrls();
- }
-
- private void setRelayEvents() throws ShellyApiException {
- if (profile.settings.relays != null) {
- int num = profile.isRoller ? profile.numRollers : profile.numRelays;
- for (int i = 0; i < num; i++) {
- setEventUrls(i);
- }
- }
- }
-
- private void setDimmerEvents() throws ShellyApiException {
- if (profile.settings.dimmers != null) {
- for (int i = 0; i < profile.settings.dimmers.size(); i++) {
- setEventUrls(i);
- }
- } else if (profile.isLight) {
- setEventUrls(0);
- }
- }
-
- /**
- * Set sensor Action URLs
- *
- * @throws ShellyApiException
- */
- private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
- if (profile.isSensor) {
- logger.debug("{}: Set Sensor Reporting URL", thingName);
- setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
- SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
- SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
- SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
- }
- }
-
- /**
- * Set/delete Relay/Roller/Dimmer Action URLs
- *
- * @param index Device Index (0-based)
- * @throws ShellyApiException
- */
- private void setEventUrls(Integer index) throws ShellyApiException {
- if (profile.isRoller) {
- setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
- SHELLY_EVENT_ROLLER_STOP);
- } else if (profile.isDimmer) {
- // 2 set of URLs
- setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
- SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
- setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
- SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
-
- // Relay output
- setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
- } else if (profile.hasRelays) {
- // Standard relays: btn_xxx, out_xxx, short/longpush URLs
- setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
- setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
- setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
- } else if (profile.isLight) {
- // Duo, Bulb
- setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
- }
- }
-
- private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
- if (config.localIp.isEmpty()) {
- throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
- }
- for (String eventType : eventTypes) {
- if (profile.containsEventUrl(eventType)) {
- // H&T adds the type=xx to report_url itself, so we need to ommit here
- String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
- String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
- String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
- + profile.thingName + "/" + eclass + urlParm;
- String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
- String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
- if (!enabled && !profile.settingsJson.contains(testUrl)) {
- // Don't set URL to null when the current one doesn't point to this OH
- // Don't interfere with a 3rd party App
- continue;
- }
- if (!profile.settingsJson.contains(testUrl)) {
- // Current Action URL is != new URL
- logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
- request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
- }
- }
- }
- }
-
- private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
- throws ShellyApiException {
- for (String eventType : eventTypes) {
- if (profile.containsEventUrl(eventType)) {
- String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
- + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
- String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
- String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
- if (!enabled && !profile.settingsJson.contains(test)) {
- // Don't set URL to null when the current one doesn't point to this OH
- // Don't interfere with a 3rd party App
- continue;
- }
- test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
- if (!profile.settingsJson.contains(test)) {
- // Current Action URL is != new URL
- logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
- request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + "="
- + urlEncode(newUrl));
- }
- }
- }
- }
-
- private static String mkEventUrl(String eventType) {
- return eventType + SHELLY_EVENTURL_SUFFIX;
- }
-
- /**
- * Submit GET request and return response, check for invalid responses
- *
- * @param uri: URI (e.g. "/settings")
- */
- public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
- String json = request(uri);
- return fromJson(gson, json, classOfT);
- }
-
- private String request(String uri) throws ShellyApiException {
- ShellyApiResult apiResult = new ShellyApiResult();
- int retries = 3;
- boolean timeout = false;
- while (retries > 0) {
- try {
- apiResult = innerRequest(HttpMethod.GET, uri);
- if (timeout) {
- logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
- apiResult.getUrl());
- timeoutsRecovered++;
- }
- return apiResult.response; // successful
- } catch (ShellyApiException e) {
- if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
- // Sensor in sleep mode or API exception for non-battery device or retry counter expired
- throw e; // non-timeout exception
- }
-
- timeout = true;
- retries--;
- timeoutErrors++; // count the retries
- logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
- }
- }
- throw new ShellyApiException("API Timeout or inconsistent result"); // successful
- }
-
- private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
- Request request = null;
- String url = "http://" + config.deviceIp + uri;
- ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
-
- try {
- request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
- TimeUnit.MILLISECONDS);
-
- if (!config.userId.isEmpty()) {
- String value = config.userId + ":" + config.password;
- request.header(HTTP_HEADER_AUTH,
- HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
- }
- request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
- logger.trace("{}: HTTP {} for {}", thingName, method, url);
-
- // Do request and get response
- ContentResponse contentResponse = request.send();
- apiResult = new ShellyApiResult(contentResponse);
- String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
- logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
-
- // validate response, API errors are reported as Json
- if (contentResponse.getStatus() != HttpStatus.OK_200) {
- throw new ShellyApiException(apiResult);
- }
- if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
- && !url.contains("/sta_cache_reset")) {
- throw new ShellyApiException("Unexpected response: " + response);
- }
- } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
- ShellyApiException ex = new ShellyApiException(apiResult, e);
- if (!ex.isTimeout()) { // will be handled by the caller
- logger.trace("{}: API call returned exception", thingName, ex);
- }
- throw ex;
- }
- return apiResult;
- }
-
- public String getControlUriPrefix(Integer id) {
- String uri = "";
- if (profile.isLight || profile.isDimmer) {
- if (profile.isDuo || profile.isDimmer) {
- // Duo + Dimmer
- uri = SHELLY_URL_CONTROL_LIGHT;
- } else {
- // Bulb + RGBW2
- uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
- }
- } else {
- // Roller, Relay
- uri = SHELLY_URL_CONTROL_RELEAY;
- }
- uri = uri + "/" + id;
- return uri;
- }
-
- @Override
- public int getTimeoutErrors() {
- return timeoutErrors;
- }
-
- @Override
- public int getTimeoutsRecovered() {
- return timeoutsRecovered;
- }
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * {@link ShellyHttpClient} implements basic HTTP access
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class ShellyHttpClient {
+ private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
+
+ public static final String HTTP_HEADER_AUTH = "Authorization";
+ public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
+ public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
+ public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
+
+ protected final HttpClient httpClient;
+ protected ShellyThingConfiguration config = new ShellyThingConfiguration();
+ protected String thingName;
+ protected final Gson gson = new Gson();
+ protected int timeoutErrors = 0;
+ protected int timeoutsRecovered = 0;
+ private ShellyDeviceProfile profile;
+
+ public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
+ this(thingName, thing.getThingConfig(), thing.getHttpClient());
+ this.profile = thing.getProfile();
+ profile.initFromThingType(thingName);
+ }
+
+ public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+ profile = new ShellyDeviceProfile();
+ this.thingName = thingName;
+ setConfig(thingName, config);
+ this.httpClient = httpClient;
+ }
+
+ public void initialize() throws ShellyApiException {
+ }
+
+ public void setConfig(String thingName, ShellyThingConfiguration config) {
+ this.thingName = thingName;
+ this.config = config;
+ }
+
+ /**
+ * Submit GET request and return response, check for invalid responses
+ *
+ * @param uri: URI (e.g. "/settings")
+ */
+ public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
+ String json = httpRequest(uri);
+ return fromJson(gson, json, classOfT);
+ }
+
+ public <T> T postApi(String uri, String data, Class<T> classOfT) throws ShellyApiException {
+ String json = httpPost(uri, data);
+ return fromJson(gson, json, classOfT);
+ }
+
+ protected String httpRequest(String uri) throws ShellyApiException {
+ ShellyApiResult apiResult = new ShellyApiResult();
+ int retries = 3;
+ boolean timeout = false;
+ while (retries > 0) {
+ try {
+ apiResult = innerRequest(HttpMethod.GET, uri, "");
+ if (timeout) {
+ logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
+ apiResult.getUrl());
+ timeoutsRecovered++;
+ }
+ return apiResult.response; // successful
+ } catch (ShellyApiException e) {
+ if ((!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound() || profile.hasBattery
+ || (retries == 0)) {
+ // Sensor in sleep mode or API exception for non-battery device or retry counter expired
+ throw e; // non-timeout exception
+ }
+
+ timeout = true;
+ retries--;
+ timeoutErrors++; // count the retries
+ logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
+ }
+ }
+ throw new ShellyApiException("API Timeout or inconsistent result"); // successful
+ }
+
+ public String httpPost(String uri, String data) throws ShellyApiException {
+ return innerRequest(HttpMethod.POST, uri, data).response;
+ }
+
+ private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException {
+ Request request = null;
+ String url = "http://" + config.deviceIp + uri;
+ ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
+
+ try {
+ request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) {
+ String value = config.userId + ":" + config.password;
+ request.header(HTTP_HEADER_AUTH,
+ HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
+ }
+ fillPostData(request, data);
+ logger.trace("{}: HTTP {} for {} {}", thingName, method, url, data);
+
+ // Do request and get response
+ ContentResponse contentResponse = request.send();
+ apiResult = new ShellyApiResult(contentResponse);
+ apiResult.httpCode = contentResponse.getStatus();
+ String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
+ logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+
+ HttpFields headers = contentResponse.getHeaders();
+ String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
+ if (!getString(auth).isEmpty()) {
+ apiResult.authResponse = auth;
+ }
+
+ // validate response, API errors are reported as Json
+ if (apiResult.httpCode != HttpStatus.OK_200) {
+ throw new ShellyApiException(apiResult);
+ }
+
+ if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
+ && !url.contains("/sta_cache_reset")) {
+ throw new ShellyApiException("Unexpected response: " + response);
+ }
+ } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
+ ShellyApiException ex = new ShellyApiException(apiResult, e);
+ if (!ex.isTimeout()) { // will be handled by the caller
+ logger.trace("{}: API call returned exception", thingName, ex);
+ }
+ throw ex;
+ }
+ return apiResult;
+ }
+
+ /**
+ * Fill in POST data, set http headers
+ *
+ * @param request HTTP request structure
+ * @param data POST data, might be empty
+ */
+ private void fillPostData(Request request, String data) {
+ boolean json = data.startsWith("{") || data.contains("\": {");
+ String type = json ? CONTENT_TYPE_JSON : CONTENT_TYPE_FORM_URLENC;
+ request.header(HttpHeader.CONTENT_TYPE, type);
+ if (!data.isEmpty()) {
+ StringContentProvider postData;
+ postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
+ request.content(postData);
+ request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
+ }
+ }
+
+ /**
+ * Format POST body depending on content type (JSON or form encoded)
+ *
+ * @param dataMap Field list
+ * @param json true=JSON format, false=form encoded
+ * @return formatted body
+ */
+ public static String buildPostData(Map<String, String> dataMap, boolean json) {
+ String data = "";
+ for (Map.Entry<String, String> e : dataMap.entrySet()) {
+ data = data + (data.isEmpty() ? "" : json ? ", " : "&");
+ if (!json) {
+ data = data + e.getKey() + "=" + e.getValue();
+ } else {
+ data = data + "\"" + e.getKey() + "\" : \"" + e.getValue() + "\"";
+ }
+ }
+ return json ? "{ " + data + " }" : data;
+ }
+
+ public String getControlUriPrefix(Integer id) {
+ String uri = "";
+ if (profile.isLight || profile.isDimmer) {
+ if (profile.isDuo || profile.isDimmer) {
+ // Duo + Dimmer
+ uri = SHELLY_URL_CONTROL_LIGHT;
+ } else {
+ // Bulb + RGBW2
+ uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
+ }
+ } else {
+ // Roller, Relay
+ uri = SHELLY_URL_CONTROL_RELEAY;
+ }
+ uri = uri + "/" + id;
+ return uri;
+ }
+
+ public int getTimeoutErrors() {
+ return timeoutErrors;
+ }
+
+ public int getTimeoutsRecovered() {
+ return timeoutsRecovered;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
+import org.openhab.core.thing.CommonTriggerEvents;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Shelly1ApiJsonDTO} is used for the JSon/GSon mapping
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class Shelly1ApiJsonDTO {
+ public static final String SHELLY_NULL_URL = "null";
+ public static final String SHELLY_URL_DEVINFO = "/shelly";
+ public static final String SHELLY_URL_STATUS = "/status";
+ public static final String SHELLY_URL_SETTINGS = "/settings";
+ public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap";
+ public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta";
+ public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta";
+ public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
+ public static final String SHELLY_URL_LIST_IR = "/ir/list";
+ public static final String SHELLY_URL_SEND_IR = "/ir/emit";
+ public static final String SHELLY_URL_RESTART = "/reboot";
+
+ public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
+ public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
+ public static final String SHELLY_URL_CONTROL_RELEAY = "/relay";
+
+ public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter";
+ public static final String SHELLY_URL_STATUS_EMETER = "/emeter";
+ public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv";
+
+ public static final String SHELLY_URL_CONTROL_ROLLER = "/roller";
+ public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller";
+
+ public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light";
+ public static final String SHELLY_URL_STATUS_LIGHT = "/light";
+ public static final String SHELLY_URL_CONTROL_LIGHT = "/light";
+
+ public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light";
+
+ // Wakeup reasons
+ public static final String SHELLY_WAKEUPT_SENSOR = "SENSOR"; // new sensordata
+ public static final String SHELLY_WAKEUPT_PERIODIC = "PERIODIC"; // periodic wakeup
+ public static final String SHELLY_WAKEUPT_BUTTON = "BUTTON"; // button pressed
+ public static final String SHELLY_WAKEUPT_POWERON = "POWERON"; // device powered up
+ public static final String SHELLY_WAKEUPT_EXT_POWER = "EXT_POWER"; // charger connected
+ public static final String SHELLY_WAKEUPT_UNKNOWN = "UNKNOWN"; // other event
+
+ //
+ // Action URLs according to the device type
+ //
+ public static final String SHELLY_EVENTURL_SUFFIX = "_url";
+
+ // Relay
+ public static final String SHELLY_EVENT_BTN_ON = "btn_on";
+ public static final String SHELLY_EVENT_BTN_OFF = "btn_off";
+ public static final String SHELLY_EVENT_OUT_ON = "out_on";
+ public static final String SHELLY_EVENT_OUT_OFF = "out_off";
+ public static final String SHELLY_EVENT_SHORTPUSH = "shortpush";
+ public static final String SHELLY_EVENT_LONGPUSH = "longpush";
+ // Button
+ public static final String SHELLY_EVENT_DOUBLE_SHORTPUSH = "double_shortpush";
+ public static final String SHELLY_EVENT_TRIPLE_SHORTPUSH = "triple_shortpush";
+ public static final String SHELLY_EVENT_SHORT_LONGTPUSH = "shortpush_longpush";
+ public static final String SHELLY_EVENT_LONG_SHORTPUSH = "longpush_shortpush";
+
+ // Dimmer
+ public static final String SHELLY_EVENT_BTN1_ON = "btn1_on";
+ public static final String SHELLY_EVENT_BTN1_OFF = "btn1_off";
+ public static final String SHELLY_EVENT_BTN2_ON = "btn2_on";
+ public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off";
+ public static final String SHELLY_EVENT_SHORTPUSH1 = "btn1_shortpush";
+ public static final String SHELLY_EVENT_LONGPUSH1 = "btn1_longpush";
+ public static final String SHELLY_EVENT_SHORTPUSH2 = "btn2_shortpush";
+ public static final String SHELLY_EVENT_LONGPUSH2 = "btn2_longpush";
+
+ // Roller
+ public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open";
+ public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close";
+ public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop";
+ public static final String SHELLY_EVENT_ROLLER_CALIB = "roller_calibrating";
+
+ // Roller states
+ public static final String SHELLY_RSTATE_OPEN = "open";
+ public static final String SHELLY_RSTATE_STOP = "stop";
+ public static final String SHELLY_RSTATE_CLOSE = "close";
+
+ // Sensors
+ public static final String SHELLY_EVENT_SENSORREPORT = "report";
+ public static final String SHELLY_EVENT_DARK = "dark";
+ public static final String SHELLY_EVENT_TWILIGHT = "twilight";
+ public static final String SHELLY_EVENT_BRIGHT = "bright";
+ public static final String SHELLY_EVENT_FLOOD_DETECTED = "flood_detected";
+ public static final String SHELLY_EVENT_FLOOD_GONE = "flood_gone";
+ public static final String SHELLY_EVENT_VIBRATION = "vibration"; // DW 1.6.5+
+ public static final String SHELLY_EVENT_OPEN = "open"; // DW 1.6.5+
+ public static final String SHELLY_EVENT_CLOSE = "close"; // DW 1.6.5+
+ public static final String SHELLY_EVENT_TEMP_OVER = "temp_over"; // FW 1.7
+ public static final String SHELLY_EVENT_TEMP_UNDER = "temp_under"; // FW 1.7
+
+ // Gas
+ public static final String SHELLY_EVENT_ALARM_MILD = "alarm_mild"; // DW 1.7+
+ public static final String SHELLY_EVENT_ALARM_HEAVY = "alarm_heavy"; // DW 1.7+
+ public static final String SHELLY_EVENT_ALARM_OFF = "alarm_off"; // DW 1.7+
+
+ //
+ // API values
+ //
+ public static final double SHELLY_API_INVTEMP = -999.0;
+
+ public static final String SHELLY_BTNT_MOMENTARY = "momentary";
+ public static final String SHELLY_BTNT_MOM_ON_RELEASE = "momentary_on_release";
+ public static final String SHELLY_BTNT_ONE_BUTTON = "one_button";
+ public static final String SHELLY_BTNT_TWO_BUTTON = "dual_button";
+ public static final String SHELLY_BTNT_TOGGLE = "toggle";
+ public static final String SHELLY_BTNT_EDGE = "edge";
+ public static final String SHELLY_BTNT_DETACHED = "detached";
+
+ public static final String SHELLY_STATE_LAST = "last";
+ public static final String SHELLY_STATE_STOP = "stop";
+
+ public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose";
+ public static final String SHELLY_INP_MODE_ONEBUTTON = "onebutton";
+
+ public static final String SHELLY_OBSTMODE_DISABLED = "disabled";
+ public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening";
+
+ public static final String SHELLY_ALWD_TRIGGER_NONE = "none";
+ public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open";
+ public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close";
+ public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop";
+
+ // API Error Codes
+ public static final String SHELLY_APIERR_UNAUTHORIZED = "Unauthorized";
+ public static final String SHELLY_APIERR_TIMEOUT = "Timeout";
+ public static final String SHELLY_APIERR_NOT_CALIBRATED = "Not calibrated!";
+
+ // API device types / properties
+ public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode
+ public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode
+ public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode
+ public static final String SHELLY_CLASS_EMETER = "emeter"; // EM/EM3: emeter
+
+ public static final String SHELLY_API_ON = "on";
+ public static final String SHELLY_API_OFF = "off";
+ public static final String SHELLY_API_TRUE = "true";
+ public static final String SHELLY_API_FALSE = "false";
+
+ public static final String SHELLY_API_MODE = "mode";
+ public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode
+ public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode
+ public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode
+ public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode
+
+ public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable";
+ public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable";
+
+ public static final String SHELLY_API_STOPR_NORMAL = "normal";
+ public static final String SHELLY_API_STOPR_SAFETYSW = "safety_switch";
+ public static final String SHELLY_API_STOPR_OBSTACLE = "obstacle";
+ public static final String SHELLY_API_STOPR_OVERPOWER = "overpower";
+
+ public static final String SHELLY_TIMER_AUTOON = "auto_on";
+ public static final String SHELLY_TIMER_AUTOOFF = "auto_off";
+ public static final String SHELLY_TIMER_ACTIVE = "has_timer";
+
+ public static final String SHELLY_LIGHT_TURN = "turn";
+ public static final String SHELLY_LIGHT_DEFSTATE = "def_state";
+ public static final String SHELLY_LIGHTTIMER = "timer";
+
+ public static final String SHELLY_COLOR_RED = "red";
+ public static final String SHELLY_COLOR_BLUE = "blue";
+ public static final String SHELLY_COLOR_GREEN = "green";
+ public static final String SHELLY_COLOR_YELLOW = "yellow";
+ public static final String SHELLY_COLOR_WHITE = "white";
+ public static final String SHELLY_COLOR_GAIN = "gain";
+ public static final String SHELLY_COLOR_BRIGHTNESS = "brightness";
+ public static final String SHELLY_COLOR_TEMP = "temp";
+ public static final String SHELLY_COLOR_EFFECT = "effect";
+
+ public static final int SHELLY_MIN_ROLLER_POS = 0;
+ public static final int SHELLY_MAX_ROLLER_POS = 100;
+ public static final int SHELLY_MIN_BRIGHTNESS = 0;
+ public static final int SHELLY_MAX_BRIGHTNESS = 100;
+ public static final int SHELLY_MIN_GAIN = 0;
+ public static final int SHELLY_MAX_GAIN = 100;
+ public static final int SHELLY_MIN_COLOR = 0;
+ public static final int SHELLY_MAX_COLOR = 255;
+ public static final int SHELLY_DIM_STEPSIZE = 10;
+
+ // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100
+ public static final int MIN_COLOR_TEMP_BULB = 3000;
+ public static final int MAX_COLOR_TEMP_BULB = 6500;
+ public static final int MIN_COLOR_TEMP_DUO = 2700;
+ public static final int MAX_COLOR_TEMP_DUO = 6500;
+ public static final int COLOR_TEMP_RANGE_BULB = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
+ public static final int COLOR_TEMP_RANGE_DUO = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
+ public static final double MIN_BRIGHTNESS = 0.0;
+ public static final double MAX_BRIGHTNESS = 100.0;
+ public static final double SATURATION_FACTOR = 2.55;
+ public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100;
+ public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100;
+
+ // Door/Window
+ public static final String SHELLY_API_ILLUM_DARK = "dark";
+ public static final String SHELLY_API_ILLUM_TWILIGHT = "twilight";
+ public static final String SHELLY_API_ILLUM_BRIGHT = "bright";
+ public static final String SHELLY_API_DWSTATE_OPEN = "open";
+ public static final String SHELLY_API_DWSTATE_CLOSE = "close";
+
+ // Shelly Sense
+ public static final String SHELLY_IR_CODET_STORED = "stored";
+ public static final String SHELLY_IR_CODET_PRONTO = "pronto";
+ public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex";
+
+ // Bulb/Duo/RGBW2
+ public static final int SHELLY_MIN_EFFECT = 0;
+ public static final int SHELLY_MAX_EFFECT = 6;
+
+ // Button
+ public static final String SHELLY_BTNEVENT_1SHORTPUSH = "S";
+ public static final String SHELLY_BTNEVENT_2SHORTPUSH = "SS";
+ public static final String SHELLY_BTNEVENT_3SHORTPUSH = "SSS";
+ public static final String SHELLY_BTNEVENT_LONGPUSH = "L";
+ public static final String SHELLY_BTNEVENT_SHORTLONGPUSH = "SL";
+ public static final String SHELLY_BTNEVENT_LONGSHORTPUSH = "LS";
+
+ public static final String SHELLY_TEMP_CELSIUS = "C";
+ public static final String SHELLY_TEMP_FAHRENHEIT = "F";
+
+ // Motion
+ public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset
+
+ // TRV
+ public static final int SHELLY_TRV_MIN_TEMP = 5; // < 5: means: lowest (valve fully closed)
+ public static final int SHELLY_TRV_MAX_TEMP = 30; // > 30: means: highest (valve fully open)
+
+ public static final String SHELLY_TRV_MODE_MANUAL = "manual";
+ public static final String SHELLY_TRV_MODE_AUTO = "automatic";
+
+ // CoIoT Multicast setting
+ public static final String SHELLY_COIOT_MCAST = "mcast";
+
+ public static class ShellySettingsDevice {
+ public String type;
+ public String mac;
+ public String hostname;
+ public String fw;
+ public Boolean auth;
+ public Integer gen;
+
+ @SerializedName("coiot") // Shelly Motion Multicast Endpoint
+ public String coiot;
+ public Integer longid;
+
+ @SerializedName("num_outputs")
+ public Integer numOutputs;
+ @SerializedName("num_meters")
+ public Integer numMeters;
+ @SerializedName("num_emeters")
+ public Integer numEMeters;
+ @SerializedName("num_rollers")
+ public Integer numRollers;
+ }
+
+ public static class ShellySettingsWiFiAp {
+ public Boolean enabled;
+ public String ssid;
+ public String key;
+ }
+
+ public static class ShellySettingsWiFiNetwork {
+ public Boolean enabled;
+ public String ssid;
+ public Integer rssi;
+
+ @SerializedName("ipv4_method")
+ public String ipv4Method;
+ public String ip;
+ public String gw;
+ public String mask;
+ public String dns;
+ }
+
+ public static class ShellySettingsMqtt {
+ public Boolean enable;
+ public String server;
+ public String user;
+ @SerializedName("reconnect_timeout_max")
+ public Double reconnectTimeoutMax;
+ @SerializedName("reconnect_timeout_min")
+ public Double reconnectTimeoutMin;
+ @SerializedName("clean_session")
+ public Boolean cleanSession;
+ @SerializedName("keep_alive")
+ public Integer keepAlive;
+ @SerializedName("will_topic")
+ public String willTopic;
+ @SerializedName("will_message")
+ public String willMessage;
+ @SerializedName("max_qos")
+ public Integer maxQOS;
+ public Boolean retain;
+ @SerializedName("update_period")
+ public Integer updatePeriod;
+ }
+
+ public static class ShellySettingsCoiot { // FW 1.6+
+ @SerializedName("update_period")
+ public Integer updatePeriod;
+ public Boolean enabled; // Motion 1.0.7: Coap can be disabled
+ public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast
+ }
+
+ public static class ShellyStatusMqtt {
+ public Boolean connected;
+ }
+
+ public static class ShellySettingsSntp {
+ public String server;
+ public Boolean enabled;
+ }
+
+ public static class ShellySettingsLogin {
+ public Boolean enabled;
+ public Boolean unprotected;
+ public String username;
+ public String password;
+ }
+
+ public static class ShellySettingsBuildInfo {
+ @SerializedName("build_id")
+ public String buildId;
+ @SerializedName("build_timestamp")
+ public String buildTimestamp;
+ @SerializedName("build_version")
+ public String buildVersion;
+ }
+
+ public static class ShellyStatusCloud {
+ public Boolean enabled;
+ public Boolean connected;
+ }
+
+ public static class ShellySettingsHwInfo {
+ @SerializedName("hw_revision")
+ public String hwRevision;
+ @SerializedName("batch_id")
+ public Integer batchId;
+ }
+
+ public static class ShellySettingsScheduleRules {
+ }
+
+ public static class ShellySettingsRelay {
+ public String name;
+ @SerializedName("default_state")
+ public String defaultState; // Accepted values: off, on, last, switch
+ @SerializedName("btn_type")
+ public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("btn1_type") // Shelly 1L
+ public String btnType1;
+ @SerializedName("btn2_type") // Shelly 1L
+ public String btnType2;
+ @SerializedName("has_timer")
+ public Boolean hasTimer; // Whether a timer is currently armed for this channel
+ @SerializedName("auto_on")
+ public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+ @SerializedName("auto_off")
+ public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+ @SerializedName("btn_on_url")
+ public String btnOnUrl; // input is activated
+ @SerializedName("btnOffUrl")
+ public String btnOffUrl; // input is deactivated
+ @SerializedName("out_on_url")
+ public String outOnUrl; // output is activated
+ @SerializedName("out_off_url")
+ public String outOffUrl; // output is deactivated
+ @SerializedName("roller_open_url")
+ public String rollerOpenUrl; // to access when roller reaches open position
+ @SerializedName("roller_close_url")
+ public String rollerCloseUrl; // to access when roller reaches close position
+ @SerializedName("roller_stop_url")
+ public String rollerStopUrl; // to access when roller stopped
+ @SerializedName("longpush_url")
+ public String pushLongUrl; // to access when roller stopped
+ @SerializedName("shortpush_url")
+ public String pushShortUrl; // to access when roller stopped
+
+ // Status information
+ public Boolean ison;
+ public Boolean overpower;
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ @SerializedName("ext_temperature")
+ public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
+ @SerializedName("ext_humidity")
+ public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
+ }
+
+ public static class ShellySettingsDimmer {
+ public String name; // unique name of the device
+ public Boolean ison; // true: output is ON
+ @SerializedName("default_state")
+ public String defaultState; // Accepted values: off, on, last, switch
+ @SerializedName("auto_on")
+ public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+ @SerializedName("auto_off")
+ public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+ @SerializedName("btn1_on_url")
+ public String btn1OnUrl; // URL to access when SW input is activated
+ @SerializedName("btn1_off_url")
+ public String btn1OffUrl; // URL to access when SW input is deactivated
+ @SerializedName("btn2_on_url")
+ public String btn2OnUrl; // URL to access when SW input is activated
+ @SerializedName("btn2_off_url")
+ public String btn2OoffUrl; // URL to access when SW input is deactivated
+ @SerializedName("out_on_url")
+ public String outOnUrl; // URL to access when output is activated
+ @SerializedName("out_off_url")
+ public String outOffUrl; // URL to access when output is deactivated
+ @SerializedName("longpush_url")
+ public String pushLongUrl; // long push button event
+ @SerializedName("shortpush_url")
+ public String pushShortUrl; // short push button event
+ @SerializedName("btn_type")
+ public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("btn1_type")
+ public String btnType1; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("btn2_type")
+ public String btnType2; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("swap_inputs")
+ public Integer swapInputs; // 0=no
+ }
+
+ public static class ShellySettingsRoller {
+ public Double maxtime;
+ @SerializedName("maxtime_open")
+ public Double maxtimeOpen;
+ @SerializedName("maxtime_close")
+ public Double maxtimeClose;
+ @SerializedName("default_state")
+ public String defaultState; // see SHELLY_STATE_xxx
+ public Boolean swap;
+ @SerializedName("swap_inputs")
+ public Boolean swapInputs;
+ @SerializedName("input_mode")
+ public String inputMode; // see SHELLY_INP_MODE_OPENCLOSE
+ @SerializedName("button_type")
+ public String buttonType; // // see SHELLY_BTNT_xxx
+ @SerializedName("btn_Reverse")
+ public Integer btnReverse;
+ public String state;
+ public Double power;
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ @SerializedName("safety_switch")
+ public Boolean safetySwitch;
+ @SerializedName("obstacle_mode")
+ public String obstaclMode; // SHELLY_OBSTMODE_
+ @SerializedName("obstacle_action")
+ public String obstacleAction; // see SHELLY_STATE_xxx
+ @SerializedName("obstacle_power")
+ public Integer obstaclePower;
+ @SerializedName("obstacle_delay")
+ public Integer obstacleDelay;
+ @SerializedName("safety_mode")
+ public String safetyMode; // see SHELLY_SAFETYM_xxx
+ @SerializedName("safety_action")
+ public String safetyAction; // see SHELLY_STATE_xxx
+ @SerializedName("safety_allowed_on_trigger")
+ public String safetyAllowedOnTrigger; // see SHELLY_ALWD_TRIGGER_xxx
+ @SerializedName("off_power")
+ public Integer offPower;
+ public Boolean positioning;
+ }
+
+ public static class ShellySettingsRgbwLight {
+ public String name;
+ public Boolean ison; // true: output is ON
+ public Integer brightness;
+ public Integer temp;
+ public Integer transition;
+ @SerializedName("default_state")
+ public String defaultState;
+ @SerializedName("auto_on")
+ public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+ @SerializedName("auto_off")
+ public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+ public Boolean schedule;
+ @SerializedName("btn_type")
+ public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("btn_reverse")
+ public Integer btnReverse; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+ @SerializedName("out_on_url")
+ public String outOnUrl; // output is activated
+ @SerializedName("out_off_url")
+ public String outOffUrl; // output is deactivated
+ }
+
+ public static class ShellyFavPos { // FW 1.9.2+ in roller mode
+ public String name;
+ public Integer pos;
+ }
+
+ public static class ShellyInputState {
+ public Integer input;
+
+ // Shelly Button
+ public String event;
+ @SerializedName("event_cnt")
+ public Integer eventCount;
+ }
+
+ public static class ShellySettingsMeter {
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ public Double power;
+ public Double[] counters = { 0.0, 0.0, 0.0 };
+ public Double total;
+ public Long timestamp;
+ }
+
+ public static class ShellySettingsEMeter { // ShellyEM meter
+ @SerializedName("is_valid")
+ public Boolean isValid; // Whether the associated meter is functioning properly
+ public Double power; // Instantaneous power, Watts
+ public Double reactive; // Instantaneous reactive power, Watts
+ public Double voltage; // RMS voltage, Volts
+ public Double total; // Total consumed energy, Wh
+ @SerializedName("total_returned")
+ public Double totalReturned; // Total returned energy, Wh
+
+ public Double pf; // 3EM
+ public Double current; // 3EM
+ }
+
+ public static class ShellySettingsUpdate {
+ public String status;
+ @SerializedName("has_update")
+ public Boolean hasUpdate;
+ @SerializedName("new_version")
+ public String newVersion;
+ @SerializedName("old_version")
+ public String oldVersion;
+ @SerializedName("beta_version")
+ public String betaVersion;
+ }
+
+ public static class ShellySettingsGlobal {
+ // https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
+ public ShellySettingsDevice device;
+ @SerializedName("wifi_ap")
+ public ShellySettingsWiFiAp wifiAp;
+ @SerializedName("wifi_sta")
+ public ShellySettingsWiFiNetwork wifiSta;
+ @SerializedName("wifi_sta1")
+ public ShellySettingsWiFiNetwork wifiSta1;
+ @SerializedName("wifirecovery_reboot_enabled")
+ public Boolean wifiRecoveryReboot; // FW 1.10+
+ @SerializedName("ap_roaming")
+ public ShellyApRoaming apRoaming; // FW 1.10+
+
+ public ShellySettingsMqtt mqtt; // not used for now
+ public ShellySettingsSntp sntp; // not used for now
+ public ShellySettingsCoiot coiot; // Firmware 1.6+
+ public ShellySettingsLogin login;
+ @SerializedName("pin_code")
+ public String pinCode;
+ @SerializedName("coiot_execute_enable")
+ public Boolean coiotExecuteEnable;
+ public String name;
+ public Boolean discoverable; // FW 1.6+
+ public String fw;
+ @SerializedName("build_info")
+ public ShellySettingsBuildInfo buildInfo;
+ public ShellyStatusCloud cloud;
+ @SerializedName("sleep_mode")
+ public ShellySensorSleepMode sleepMode; // FW 1.6
+ @SerializedName("external_power")
+ public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
+ @SerializedName("debug_enable") // FW 1.10+
+ public Boolean debugEnable;
+
+ public String timezone;
+ public Double lat;
+ public Double lng;
+ public Boolean tzautodetect;
+ public String time;
+
+ public ShellySettingsHwInfo hwinfo;
+ public String mode;
+ @SerializedName("max_power")
+ public Double maxPower;
+ public Boolean calibrated;
+
+ public ArrayList<ShellySettingsRelay> relays;
+ public ArrayList<ShellySettingsRoller> rollers;
+ public ArrayList<ShellySettingsDimmer> dimmers;
+ public ArrayList<ShellySettingsRgbwLight> lights;
+ public ArrayList<ShellySettingsEMeter> emeters;
+ public ArrayList<ShellySettingsInput> inputs; // ix3
+ public ArrayList<ShellyThermnostat> thermostats; // TRV
+
+ public Double voltage; // AC voltage for Shelly 2.5
+ @SerializedName("supply_voltage")
+ public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
+
+ @SerializedName("temperature_units")
+ public String temperatureUnits = "C"; // Either'C'or'F'
+
+ @SerializedName("led_status_disable")
+ public Boolean ledStatusDisable; // PlugS only Disable LED indication for network
+ // status
+ @SerializedName("led_power_disable")
+ public Boolean ledPowerDisable; // PlugS only Disable LED indication for network
+ // status
+ @SerializedName("light_sensor")
+ public String lightSensor; // Sense: sensor type
+ @SerializedName("rain_sensor")
+ public Boolean rainSensor; // Flood: true=in rain mode
+
+ // FW 1.5.7: Door Window
+ @SerializedName("dark_treshold")
+ public Integer darkTreshold; // Illumination definition for "dark" in lux
+ @SerializedName("twilight_treshold")
+ public Integer twiLightTreshold; // Illumination definition for "twilight" in lux
+ @SerializedName("dark_url")
+ public String darkUrl; // URL to report to when luminance <= dark_threshold
+ @SerializedName("twilight_url")
+ public String twiLightUrl; // URL reports when luminance > dark_threshold AND luminance <=
+ @SerializedName("close_url")
+ public String closeUrl; // URL reports when DW contact is closed FW 1.6.5+
+ @SerializedName("vibration_url")
+ public String vibrationUrl; // URL reports when DW detects vibration FW 1.6.5+
+
+ // Gas FW 1.7
+ @SerializedName("set_volume")
+ public Integer volume; // Speaker volume for alarm
+ @SerializedName("alarm_off_url")
+ public String alarmOffUrl; // URL reports when alarm went off
+ @SerializedName("alarm_mild_url")
+ public String alarmMidUrl; // URL reports middle alarm
+ @SerializedName("alarm_heavy_url")
+ public String alarmHeavyfUrl; // URL reports heavy alarm
+
+ // Roller with FW 1.9.2+
+ @SerializedName("favorites_enabled")
+ public Boolean favoritesEnabled = false;
+ public ArrayList<ShellyFavPos> favorites;
+
+ // Motion
+ public ShellyMotionSettings motion;
+ @SerializedName("tamper_sensitivity")
+ public Integer tamperSensitivity;
+ @SerializedName("dark_threshold")
+ public Integer darkThreshold;
+ @SerializedName("twilight_threshold")
+ public Integer twilightThreshold;
+
+ @SerializedName("sleep_time") // Shelly Motion
+ public Integer sleepTime;
+ }
+
+ public static class ShellySettingsAttributes {
+ @SerializedName("device_type")
+ public String deviceType; // Device model identifier
+ @SerializedName("device_mac")
+ public String deviceMac; // MAC address of the device in hexadecimal
+ @SerializedName("wifi_ap")
+ public String wifiAp; // WiFi access poInteger configuration, see /settings/ap for details
+ @SerializedName("wifi_sta")
+ public String wifiSta; // WiFi client configuration. See /settings/sta for details
+ public String login; // credentials used for HTTP Basic authentication for the REST interface. If
+ // enabled is true clients must include an Authorization: Basic ... HTTP header with valid
+ // credentials when performing TP requests.
+ public String name; // unique name of the device.
+ public String fw; // current FW version
+ }
+
+ public static class ShellyActionsStats {
+ public Integer skipped;
+ }
+
+ public static class ShellySettingsStatus {
+ public String name; // FW 1.8: Symbolic Device name is configurable
+
+ @SerializedName("wifi_sta")
+ public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
+ public ShellyStatusCloud cloud;
+ public ShellyStatusMqtt mqtt;
+
+ public String time;
+ public Integer serial = -1;
+ @SerializedName("has_update")
+ public Boolean hasUpdate;
+ public String mac;
+ public Boolean discoverable; // FW 1.6+
+ @SerializedName("cfg_changed_cnt")
+ public Integer cfgChangedCount; // FW 1.8
+ @SerializedName("actions_stats")
+ public ShellyActionsStats astats;
+
+ public ArrayList<ShellySettingsRelay> relays;
+ public Double voltage; // Shelly 2.5
+
+ public ArrayList<ShellySettingsRoller> rollers;
+ public Integer input; // RGBW2 has no JSON array
+ public ArrayList<ShellyInputState> inputs;
+ public ArrayList<ShellySettingsLight> lights;
+ public ArrayList<ShellyShortLightStatus> dimmers;
+ public ArrayList<ShellySettingsMeter> meters;
+ public ArrayList<ShellySettingsEMeter> emeters;
+
+ // Internal device temp
+ public ShellySensorTmp tmp; // Shelly 1PM
+ public Double temperature = SHELLY_API_INVTEMP; // Shelly 2.5
+ public Boolean overtemperature;
+
+ // Shelly Dimmer only
+ public Boolean loaderror;
+ public Boolean overload;
+
+ // Shelly TRV
+ public Boolean calibrated;
+ public ArrayList<ShellyThermnostat> thermostats;
+
+ public ShellySettingsUpdate update;
+ @SerializedName("ram_total")
+ public Long ramTotal;
+ @SerializedName("ram_free")
+ public Long ramFree;
+ @SerializedName("fs_size")
+ public Long fsSize;
+ @SerializedName("fs_free")
+ public Long fsFree;
+ public Long uptime;
+
+ @SerializedName("sleep_time") // Shelly Motion
+ public Integer sleepTime;
+
+ public String json;
+ }
+
+ public static class ShellySettingsInput {
+ @SerializedName("btn_type")
+ public String btnType;
+ }
+
+ public static class ShellyControlRelay {
+ // https://shelly-api-docs.shelly.cloud/#shelly1-1pm-settings-relay-0
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ @SerializedName("has_timer")
+ public Boolean hasTimer; // Whether a timer is currently armed for this channel
+ @SerializedName("timer_remaining")
+ public Integer timerRemaining; // FW 1.6+
+ public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
+
+ public String turn; // Accepted values are on and off. This will turn ON/OFF the respective output
+ // channel when request is sent .
+ public Integer timer; // A one-shot flip-back timer in seconds.
+ }
+
+ public static class ShellyShortStatusRelay {
+ public String name; // FW 1.8+: Channel could now have a logical name
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ public Boolean ison; // Whether output channel is on or off
+ @SerializedName("has_timer")
+ public Boolean hasTimer; // Whether a timer is currently armed for this channel
+ @SerializedName("timer_remaining")
+ public Integer timerRemaining;
+ public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
+ public Double temperature; // Internal device temperature
+ public Boolean overtemperature; // Device over heated
+ }
+
+ public static class ShellyShortLightStatus {
+ public Boolean ison; // Whether output channel is on or off
+ public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
+ public Integer brightness; // brightness: 0.100%
+
+ @SerializedName("has_timer")
+ public Boolean hasTimer;
+ }
+
+ public static class ShellyStatusRelay {
+ public String name; // FW 1.8: Symbolic channel name is configurable
+
+ @SerializedName("wifi_sta")
+ public ShellySettingsWiFiNetwork wifiSta; // WiFi status
+ public ShellySettingsCoiot coiot; // Firmware 1.6+
+ public Integer serial;
+ public String mac; // MAC
+ public ArrayList<ShellyShortStatusRelay> relays; // relay status
+ public ArrayList<ShellySettingsMeter> meters; // current meter value
+ public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
+
+ @SerializedName("ext_temperature")
+ public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
+ @SerializedName("ext_humidity")
+ public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
+
+ public Double temperature; // device temp acc. on the selected temp unit
+ public ShellySensorTmp tmp;
+ }
+
+ public static class ShellyStatusDimmer {
+ @SerializedName("wifi_sta")
+ public ShellySettingsWiFiNetwork wifiSta; // WiFi status
+ public ArrayList<ShellyShortLightStatus> lights; // relay status
+ public ArrayList<ShellySettingsMeter> meters; // current meter value
+
+ public ShellySensorTmp tmp;
+ public Boolean overtemperature;
+
+ public Boolean loaderror;
+ public Boolean overload;
+ }
+
+ public static class ShellyRollerStatus {
+ public String name; // FW 1.8: Symbolic name is configurable
+
+ @SerializedName("roller_pos")
+ public Integer rollerPos; // number Desired position in percent
+ public Integer duration; // If specified, the motor will move for this period in seconds. If missing, the
+ // value of maxtime in /settings/roller/N will be used.
+ public String state; // One of stop, open, close
+ public Double power; // Current power consumption in Watts
+ @SerializedName("is_valid")
+ public Boolean isValid; // If the power meter functions properly
+ @SerializedName("safety_switch")
+ public Boolean safetySwitch; // Whether the safety input is currently triggered
+ public Boolean overtemperature;
+ @SerializedName("stop_reason")
+ public String stopReason; // Last cause for stopping: normal, safety_switch, obstacle
+ @SerializedName("last_direction")
+ public String lastDirection; // Last direction of motion, open or close
+ public Boolean calibrating;
+ public Boolean positioning; // true when calibration was performed
+ @SerializedName("current_pos")
+ public Integer currentPos; // current position 0..100, 100=open
+ }
+
+ public static class ShellyOtaCheckResult {
+ public String status;
+ }
+
+ public static class ShellyApRoaming {
+ public Boolean enabled;
+ public Integer threshold;
+ }
+
+ public static class ShellySensorSleepMode {
+ public Integer period;
+ public String unit;
+ }
+
+ // Shelly TRV
+ public class ShellyThermnostat {
+ public class ShellyThermTargetTemp {
+ public Boolean enabled;
+ public Double value;
+ public String unit;
+ }
+
+ public class ShellyThermTemp {
+ public Double value;
+ public String units;
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ }
+
+ public Double pos;
+ @SerializedName("target_t")
+ public ShellyThermTargetTemp targetTemp;
+ public Boolean schedule;
+ @SerializedName("schedule_profile")
+ public Integer profile;
+ @SerializedName("schedule_profile_names")
+ public String[] profileNames;
+ public ShellyThermTemp tmp;
+ @SerializedName("boost_minutes")
+ public Integer boostMinutes;
+ }
+
+ public static class ShellySensorTmp {
+ public Double value; // Temperature in configured unites
+ public String units; // 'C' or 'F'
+ public Double tC; // temperature in deg C
+ public Double tF; // temperature in deg F
+ @SerializedName("is_valid")
+ public Boolean isValid; // whether the internal sensor is operating properly
+ }
+
+ public static class ShellyStatusSensor {
+ // https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
+ public static class ShellySensorHum {
+ public Double value; // relative humidity in %
+ }
+
+ public static class ShellySensorBat {
+ public Double value; // estimated remaining battery capacity in %
+ public Double voltage; // battery voltage
+ };
+
+ // Door/Window sensor
+ public static class ShellySensorState {
+ @SerializedName("is_valid")
+ public Boolean isValid; // whether the internal sensor is operating properly
+ public String state; // Shelly Door/Window
+
+ // Shelly Motion
+ public Boolean motion;
+ public Boolean vibration;
+ @SerializedName("timestamp")
+ public Long motionTimestamp;
+ @SerializedName("active")
+ public Boolean motionActive;
+ }
+
+ public static class ShellySensorLux {
+ @SerializedName("is_valid")
+ public Boolean isValid; // whether the internal sensor is operating properly
+ public Double value;
+
+ public String illumination;
+ }
+
+ public static class ShellySensorAccel {
+ public Integer tilt; // Tilt in °
+ public Integer vibration; // Whether vibration is detected
+ }
+
+ public static class ShellyMotionSettings {
+ public Integer sensitivity;
+ @SerializedName("blind_time_minutes")
+ public Integer blindTimeMinutes;
+ @SerializedName("pulse_count")
+ public Integer pulseCount;
+ @SerializedName("operating_mode")
+ public Integer operatingMode;
+ public Boolean enabled;
+ }
+
+ public static class ShellyExtTemperature {
+ public static class ShellyShortTemp {
+ public Double tC; // temperature in deg C
+ public Double tF; // temperature in deg F
+ }
+
+ // Shelly 1/1PM have up to 3 sensors
+ // for whatever reasons it's not an array, but 3 independent elements
+ @SerializedName("0")
+ public ShellyShortTemp sensor1;
+ @SerializedName("1")
+ public ShellyShortTemp sensor2;
+ @SerializedName("2")
+ public ShellyShortTemp sensor3;
+ }
+
+ public static class ShellyExtHumidity {
+ public static class ShellyShortHum {
+ public Double hum; // Humidity reading of sensor 0, percent
+ }
+
+ // Shelly 1/1PM have up to 3 sensors
+ // for whatever reasons it's not an array, but 3 independent elements
+ @SerializedName("0")
+ public ShellyShortHum sensor1;
+ }
+
+ public static class ShellyADC {
+ public Double voltage;
+ }
+
+ public ShellySensorTmp tmp;
+ public ShellySensorHum hum;
+ public ShellySensorLux lux;
+ public ShellySensorAccel accel;
+ public ShellySensorBat bat;
+ @SerializedName("sensor")
+ public ShellySensorState sensor;
+ public Boolean smoke; // SHelly Smoke
+ public Boolean flood; // Shelly Flood: true = flood condition detected
+ @SerializedName("rain_sensor")
+ public Boolean rainSensor; // Shelly Flood: true=in rain mode
+
+ public Boolean motion; // Shelly Sense: true=motion detected
+ public Boolean charger; // Shelly Sense, TRV: true=charger connected
+
+ @SerializedName("act_reasons")
+ public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
+
+ @SerializedName("sensor_error")
+ public String sensorError; // 1.5.7: Only displayed in case of error
+
+ // FW 1.7: Shelly Gas
+ @SerializedName("gas_sensor")
+ public ShellyStatusGasSensor gasSensor;
+ @SerializedName("concentration")
+ public ShellyStatusGasConcentration concentration;
+ public ArrayList<ShellyStatusValve> valves;
+
+ // FW 1.7 Button
+ @SerializedName("connect_retries")
+ public Integer connectRetries;
+ public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
+
+ // Shelly UNI FW 1.9+
+ public ArrayList<ShellyADC> adcs;
+
+ // Shelly TRV
+ public Boolean calibrated;
+ public ArrayList<ShellyThermnostat> thermostats;
+ }
+
+ public static class ShellySettingsSmoke {
+ @SerializedName("temperature_units")
+ public String temperatureUnits; // Either 'C' or 'F'
+ @SerializedName("temperature_threshold")
+ public Integer temperatureThreshold; // Temperature delta (in configured degree units) which triggers an update
+ @SerializedName("sleep_mode_period")
+ public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24
+ }
+
+ // Shelly Gas
+ // "gas_sensor":{"sensor_state":"normal","self_test_state":"not_completed","alarm_state":"none"},
+ // "concentration":{"ppm":0,"is_valid":true},
+ public static class ShellyStatusGasSensor {
+ @SerializedName("sensor_state")
+ public String sensorState;
+ @SerializedName("self_test_state")
+ public String selfTestState;
+ @SerializedName("alarm_state")
+ public String alarmState;
+ }
+
+ public static class ShellyStatusGasConcentration {
+ public Integer ppm;
+ @SerializedName("is_valid")
+ public Boolean isValid;
+ }
+
+ public static class ShellyStatusValve {
+ public String state; // closed/opened/not_connected/failure/closing/opening/checking
+ }
+
+ public static class ShellySettingsLight {
+ public Integer red; // red brightness, 0..255, applies in mode="color"
+ public Integer green; // green brightness, 0..255, applies in mode="color"
+ public Integer blue; // blue brightness, 0..255, applies in mode="color"
+ public Integer white; // white brightness, 0..255, applies in mode="color"
+ public Integer gain; // gain for all channels, 0..100, applies in mode="color"
+ public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
+ public Integer brightness; // brightness, 0..100, applies in mode="white"
+ public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
+ // Change, 3: Breath,
+ // 4: Flash, 5: On/Off Gradual, 6: Red/Green Change
+ @SerializedName("default_state")
+ public String defaultState; // one of on, off or last
+ @SerializedName("auto_on")
+ public Double autoOn; // see above
+ @SerializedName("auto_off")
+ public Double autoOff; // see above
+
+ public Integer dcpower; // RGW2:Set to true for 24 V power supply, false for 12 V
+
+ // Shelly Dimmer
+ public String mode;
+ public Boolean ison;
+ }
+
+ public static class ShellySettingsNightMode { // FW1.5.7+
+ public Integer enabled;
+ @SerializedName("start_time")
+ public String startTime;
+ @SerializedName("end_time")
+ public String endTime;
+ public Integer brightness;
+ }
+
+ public static class ShellyStatusLightChannel {
+ public Boolean ison;
+ public Double power;
+ public Boolean overpower;
+ @SerializedName("has_timer")
+ public Boolean hasTimer;
+ @SerializedName("timer_started")
+ public Integer timerStarted;
+ @SerializedName("timer_duration")
+ public Integer timerDuration;
+ @SerializedName("timer_remaining")
+ public Integer timerRemaining;
+
+ public Integer red; // red brightness, 0..255, applies in mode="color"
+ public Integer green; // green brightness, 0..255, applies in mode="color"
+ public Integer blue; // blue brightness, 0..255, applies in mode="color"
+ public Integer white; // white brightness, 0..255, applies in mode="color"
+ public Integer gain; // gain for all channels, 0..100, applies in mode="color"
+ public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
+ public Integer brightness; // brightness, 0..100, applies in mode="white"
+ public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
+ // Change, 3: Breath,
+ }
+
+ public static class ShellyStatusLight {
+ public Boolean ison; // Whether output channel is on or off
+ public Integer input;
+
+ public ArrayList<ShellyStatusLightChannel> lights;
+ public ArrayList<ShellySettingsMeter> meters;
+ }
+
+ public static class ShellySenseKeyCode {
+ String id; // ID of the stored IR code into Shelly Sense.
+ String name; // Short description or name of the stored IR code.
+ }
+
+ public static class ShellySendKeyList {
+ @SerializedName("key_codes")
+ public ArrayList<ShellySenseKeyCode> keyCodes;
+ }
+
+ /**
+ * Shelly Dimmer returns light[]. However, the structure doesn't match the lights[] of a Bulb/RGBW2.
+ * The tag lights[] will be replaced with dimmers[] so this could be mapped to a different Gson structure.
+ * The function requires that it's only called when the device is a dimmer - on get settings and get status
+ *
+ * @param json Input Json as received by the API
+ * @return Modified Json
+ */
+ public static String fixDimmerJson(String json) {
+ return !json.contains("\"lights\":[") ? json
+ : json.replaceFirst(java.util.regex.Pattern.quote("\"lights\":["), "\"dimmers\":[");
+ }
+
+ /**
+ * Convert Shelly Button events into OH button states
+ *
+ * @param eventType S/SS/SSS or L
+ * @return OH button states
+ */
+ public static String mapButtonEvent(String eventType) {
+ // decode different codings
+ // 0..2: CoAP
+ // S/SS/SSS/L: CoAP for Button and xi3
+ // shortpush/double_shortpush/triple_shortpush/longpush: REST
+ switch (eventType) {
+ case "0":
+ return CommonTriggerEvents.RELEASED;
+ case "1":
+ case SHELLY_BTNEVENT_1SHORTPUSH:
+ case SHELLY_EVENT_SHORTPUSH:
+ return CommonTriggerEvents.SHORT_PRESSED;
+ case SHELLY_BTNEVENT_2SHORTPUSH:
+ case SHELLY_EVENT_DOUBLE_SHORTPUSH:
+ return CommonTriggerEvents.DOUBLE_PRESSED;
+ case SHELLY_BTNEVENT_3SHORTPUSH:
+ case SHELLY_EVENT_TRIPLE_SHORTPUSH:
+ return "TRIPLE_PRESSED";
+ case "2":
+ case SHELLY_BTNEVENT_LONGPUSH:
+ case SHELLY_EVENT_LONGPUSH:
+ return CommonTriggerEvents.LONG_PRESSED;
+ case SHELLY_BTNEVENT_SHORTLONGPUSH:
+ case SHELLY_EVENT_SHORT_LONGTPUSH:
+ return "SHORT_LONG_PRESSED";
+ case SHELLY_BTNEVENT_LONGSHORTPUSH:
+ case SHELLY_EVENT_LONG_SHORTPUSH:
+ return "LONG_SHORT_PRESSED";
+ default:
+ return "";
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link Shelly1CoapListener} describes the listening interface to process Coap responses
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public interface Shelly1CoIoTInterface {
+ public int getVersion();
+
+ public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
+
+ public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
+
+ public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+ Map<String, State> updates, ShellyColorUtils col);
+
+ public String getLastWakeup();
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link Shelly1CoIoTProtocol} implements common functions for the CoIoT implementations
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTProtocol {
+ private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTProtocol.class);
+ protected final String thingName;
+ protected final ShellyThingInterface thingHandler;
+ protected final ShellyDeviceProfile profile;
+ protected final ShellyApiInterface api;
+ protected final Map<String, CoIotDescrBlk> blkMap;
+ protected final Map<String, CoIotDescrSen> sensorMap;
+ private final Gson gson = new GsonBuilder().create();
+
+ // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
+ // between a real update or just a repeated status on periodic updates
+ protected int lastCfgCount = -1;
+ protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
+ protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
+ protected String lastWakeup = "";
+
+ public Shelly1CoIoTProtocol(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+ Map<String, CoIotDescrSen> sensorMap) {
+ this.thingName = thingName;
+ this.thingHandler = thingHandler;
+ this.blkMap = blkMap;
+ this.sensorMap = sensorMap;
+ this.profile = thingHandler.getProfile();
+ this.api = thingHandler.getApi();
+ }
+
+ protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
+ Map<String, State> updates, ShellyColorUtils col) {
+ // Process status information and convert into channel updates
+ int rIndex = getIdFromBlk(sen);
+ String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+ : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+
+ switch (sen.type.toLowerCase()) {
+ case "b": // BatteryLevel +
+ updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
+ toQuantityType(s.value, 0, Units.PERCENT));
+ break;
+ case "h" /* Humidity */:
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
+ toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
+ break;
+ case "m" /* Motion */:
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+ case "l": // Luminosity +
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
+ toQuantityType(s.value, DIGITS_LUX, Units.LUX));
+ break;
+ case "s": // CatchAll
+ switch (sen.desc.toLowerCase()) {
+ case "state": // Relay status +
+ case "output":
+ updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
+ break;
+ case "input":
+ handleInput(sen, s, rGroup, updates);
+ break;
+ case "brightness":
+ // already handled by state/output
+ break;
+ case "overtemp": // ++
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+ }
+ break;
+ case "position":
+ // work around: Roller reports 101% instead max 100
+ double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+ toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+ toQuantityType(pos, Units.PERCENT));
+ break;
+ case "flood":
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+ case "vibration": // DW with FW1.6.5+
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ if (s.value == 1) {
+ thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+ EVENT_TYPE_VIBRATION);
+ }
+ break;
+ case "luminositylevel": // +
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
+ break;
+ case "charger": // Sense
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+ // RGBW2/Bulb
+ case "red":
+ col.setRed((int) s.value);
+ updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
+ ShellyColorUtils.toPercent((int) s.value));
+ break;
+ case "green":
+ col.setGreen((int) s.value);
+ updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
+ ShellyColorUtils.toPercent((int) s.value));
+ break;
+ case "blue":
+ col.setBlue((int) s.value);
+ updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
+ ShellyColorUtils.toPercent((int) s.value));
+ break;
+ case "white":
+ col.setWhite((int) s.value);
+ updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
+ ShellyColorUtils.toPercent((int) s.value));
+ break;
+ case "gain":
+ col.setGain((int) s.value);
+ updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
+ ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
+ break;
+ case "sensorerror":
+ String sensorError = s.valueStr != null ? getString(s.valueStr) : "" + s.value;
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(sensorError));
+ break;
+ default:
+ // Unknown
+ return false;
+ }
+ break;
+
+ default:
+ // Unknown type
+ return false;
+ }
+
+ return true;
+ }
+
+ public static boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
+ updates.put(mkChannelId(group, channel), value);
+ return true;
+ }
+
+ protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
+ int idx = getSensorNumber(sen.desc, sen.id) - 1;
+ String iGroup = profile.getInputGroup(idx);
+ String iChannel = CHANNEL_INPUT + profile.getInputSuffix(idx);
+ updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
+ }
+
+ protected void handleInputEvent(CoIotDescrSen sen, String type, int count, int serial, Map<String, State> updates) {
+ int idx = getSensorNumber(sen.desc, sen.id) - 1;
+ String group = profile.getInputGroup(idx);
+ if (count == -1) {
+ // event type
+ updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(idx), new StringType(type));
+ inputEvent[idx] = type;
+ } else {
+ // event count
+ updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(idx), getDecimal(count));
+ logger.trace(
+ "{}: Check button[{}] for event trigger (isButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
+ thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
+ lastEventCount[idx]);
+ if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1))
+ || ((lastEventCount[idx] != -1) && (count != lastEventCount[idx])))) {
+ if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
+ logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
+ thingHandler.triggerButton(group, idx, inputEvent[idx]);
+ }
+ }
+ lastEventCount[idx] = count;
+ }
+ }
+
+ /**
+ *
+ * Handles the combined updated of the brightness channel:
+ * brightness$Switch is the OnOffType (power state)
+ * brightness&Value is the brightness value
+ *
+ * @param profile Device profile, required to select the channel group and name
+ * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
+ * @param id Sensor id from the update
+ * @param sen Sensor description from the update
+ * @param s New sensor value
+ * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
+ */
+ protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
+ CoIotSensor s, List<CoIotSensor> allUpdates) {
+ String group = "";
+ String channel = CHANNEL_BRIGHTNESS;
+ String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
+ if (profile.isLight || profile.isDimmer) {
+ if (profile.isBulb || profile.inColor) {
+ group = CHANNEL_GROUP_LIGHT_CONTROL;
+ channel = CHANNEL_LIGHT_POWER;
+ } else if (profile.isDuo) {
+ group = CHANNEL_GROUP_WHITE_CONTROL;
+ } else if (profile.isDimmer) {
+ group = CHANNEL_GROUP_RELAY_CONTROL;
+ } else if (profile.isRGBW2) {
+ checkL = String.valueOf(id); // String.valueOf(id - 1); // id is 1-based, L is 0-based
+ group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
+ logger.trace("{}: updatePower() for L={}", thingName, checkL);
+ }
+
+ // We need to update brightness and on/off state at the same time to avoid "flipping brightness slider" in
+ // the UI
+ double brightness = -1.0;
+ double power = -1.0;
+ for (CoIotSensor update : allUpdates) {
+ CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
+ if (!checkL.isEmpty() && !d.links.equals(checkL)) {
+ // continue until we find the correct one
+ continue;
+ }
+ if (d.desc.equalsIgnoreCase("brightness")) {
+ brightness = update.value;
+ } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
+ power = update.value;
+ }
+ }
+ if (power != -1) {
+ updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
+ }
+ if (brightness != -1) {
+ updateChannel(updates, group, channel + "$Value",
+ toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
+ }
+ } else if (profile.hasRelays) {
+ group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
+ updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ } else if (profile.isSensor) {
+ // Sensor state
+ if (profile.isDW) { // Door Window has item type Contact
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+ s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+ } else {
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ }
+ }
+ }
+
+ /**
+ * Find index of Input id, which is required to map to channel name
+ *
+ * @parm sensorDesc D field from sensor update
+ * @param sensorId The id from the sensor update
+ * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
+ */
+ protected int getSensorNumber(String sensorDesc, String sensorId) {
+ int idx = 0;
+ for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
+ CoIotDescrSen sen = se.getValue();
+ if (sen.desc.equalsIgnoreCase(sensorDesc)) {
+ idx++; // iterate from input1..2..n
+ }
+ if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
+ int id = getIdFromBlk(sen);
+ if (id != -1) {
+ return id;
+ }
+ }
+ if (sen.id.equalsIgnoreCase(sensorId)) {
+ return idx;
+ }
+ }
+ logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
+ return -1;
+ }
+
+ protected int getIdFromBlk(CoIotDescrSen sen) {
+ int idx = -1;
+ CoIotDescrBlk blk = blkMap.get(sen.links);
+ if (blk != null) {
+ String desc = blk.desc.toLowerCase();
+ if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
+ || desc.startsWith(SHELLY_CLASS_LIGHT) || desc.startsWith(SHELLY_CLASS_EMETER)) {
+ if (desc.contains("_")) { // CoAP v2
+ idx = Integer.parseInt(substringAfter(desc, "_"));
+ } else { // CoAP v1
+ if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
+ idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
+ }
+ if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
+ idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
+ }
+ if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
+ idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
+ }
+ }
+ idx = idx + 1; // make it 1-based (sen.L is 0-based)
+ }
+ }
+ return idx;
+ }
+
+ /**
+ *
+ * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
+ *
+ * @param sensorId sensorId to map into a channel index
+ * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
+ */
+ protected int getExtTempId(String sensorId) {
+ int idx = 0;
+ for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
+ CoIotDescrSen sen = se.getValue();
+ if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
+ || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
+ idx++; // iterate from temperature1..2..n
+ }
+ if (sen.id.equalsIgnoreCase(sensorId)) {
+ return idx;
+ }
+ }
+ logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
+ return -1;
+ }
+
+ protected ShellyDeviceProfile getProfile() {
+ return profile;
+ }
+
+ public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+ return sen != null ? sen : new CoIotDescrSen();
+ }
+
+ public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
+ }
+
+ protected void addSensor(Map<String, CoIotDescrSen> sensorMap, String key, String json) {
+ try {
+ if (!sensorMap.containsKey(key)) {
+ CoIotDescrSen sen = gson.fromJson(json, CoIotDescrSen.class);
+ if (sen != null) {
+ sensorMap.put(key, sen);
+ }
+ }
+ } catch (JsonSyntaxException e) {
+ // should never happen
+ logger.trace("Unable to parse sensor definition: {}", json, e);
+ }
+ }
+
+ public String getLastWakeup() {
+ return lastWakeup;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 1
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTVersion1 extends Shelly1CoIoTProtocol implements Shelly1CoIoTInterface {
+ private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTVersion1.class);
+
+ public Shelly1CoIoTVersion1(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+ Map<String, CoIotDescrSen> sensorMap) {
+ super(thingName, thingHandler, blkMap, sensorMap);
+ }
+
+ @Override
+ public int getVersion() {
+ return Shelly1CoapJSonDTO.COIOT_VERSION_1;
+ }
+
+ /**
+ * Process CoIoT status update message. If a status update is received, but the device description has not been
+ * received yet a GET is send to query device description.
+ *
+ * @param devId device id included in the status packet
+ * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
+ * @param serial Serial for this request. If this the the same as last serial
+ * the update was already sent and processed so this one gets
+ * ignored.
+ */
+ @Override
+ public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+ Map<String, State> updates, ShellyColorUtils col) {
+ // first check the base implementation
+ if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
+ // process by the base class
+ return true;
+ }
+
+ // Process status information and convert into channel updates
+ Integer rIndex = Integer.parseInt(sen.links) + 1;
+ String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+ : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+ switch (sen.type.toLowerCase()) {
+ case "t": // Temperature +
+ Double value = getDouble(s.value);
+ switch (sen.desc.toLowerCase()) {
+ case "temperature": // Sensor Temp
+ if (getString(getProfile().settings.temperatureUnits)
+ .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
+ value = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(s.value))
+ .doubleValue();
+ }
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
+ toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+ break;
+ case "temperature f": // Device Temp -> ignore (we use C only)
+ break;
+ case "temperature c": // Device Temp in C
+ // Device temperature
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
+ toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
+ break;
+ case "external temperature f": // Shelly 1/1PM external temp sensors
+ // ignore F, we use C only
+ break;
+ case "external temperature c": // Shelly 1/1PM external temp sensors
+ case "external_temperature":
+ int idx = getExtTempId(sen.id);
+ if (idx > 0) {
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
+ toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+ } else {
+ logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
+ sen.desc);
+ }
+ break;
+ default:
+ logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
+ }
+ break;
+ case "p": // Power/Watt
+ // 3EM uses 1-based meter IDs, other 0-based
+ String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
+ : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
+ updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
+ toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+ updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
+ break;
+ case "s" /* CatchAll */:
+ switch (sen.desc.toLowerCase()) {
+ case "overtemp":
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+ }
+ break;
+ case "energy counter 0 [w-min]":
+ updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
+ toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+ break;
+ case "energy counter 1 [w-min]":
+ case "energy counter 2 [w-min]":
+ // we don't use them
+ break;
+ case "energy counter total [w-h]": // 3EM reports W/h
+ case "energy counter total [w-min]":
+ Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
+ updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
+ toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+ break;
+ case "voltage":
+ updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
+ toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
+ break;
+ case "current":
+ updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
+ toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
+ break;
+ case "pf":
+ updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
+ toQuantityType(getDecimal(s.value), Units.PERCENT));
+ break;
+ case "position":
+ // work around: Roller reports 101% instead max 100
+ double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+ toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+ toQuantityType(pos, Units.PERCENT));
+ break;
+ case "input event": // Shelly Button 1
+ handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
+ break;
+ case "input event counter": // Shelly Button 1/ix3
+ handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
+ break;
+ case "flood":
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+ case "tilt": // DW with FW1.6.5+ //+
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
+ toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
+ break;
+ case "vibration": // DW with FW1.6.5+
+ if (profile.isMotion) {
+ // handle as status
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ } else if (s.value == 1) {
+ // handle as event
+ thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+ EVENT_TYPE_VIBRATION);
+ }
+ break;
+ case "temp": // Shelly Bulb
+ case "colortemperature": // Shelly Duo
+ updateChannel(updates,
+ profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
+ CHANNEL_COLOR_TEMP,
+ ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
+ break;
+ case "sensor state": // Shelly Gas
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
+ break;
+ case "alarm state": // Shelly Gas
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+ getStringType(s.valueStr));
+ break;
+ case "self-test state":// Shelly Gas
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
+ getStringType(s.valueStr));
+ break;
+ case "concentration":// Shelly Gas
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
+ toQuantityType(getDouble(s.value), DIGITS_NONE, Units.PARTS_PER_MILLION));
+ break;
+ case "sensorerror":
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
+ break;
+ default:
+ // Unknown
+ return false;
+ }
+ break;
+
+ default:
+ // Unknown type
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ *
+ * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
+ * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
+ * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
+ * old mapping stays in to support older firmware releases.
+ *
+ * @param sen Sensor description received from device
+ * @return fixed Sensor description (sen)
+ */
+ @Override
+ public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+ // Shelly1: reports null descr+type "Switch" -> map to S
+ // Shelly1PM: reports null descr+type "Overtemp" -> map to O
+ // Shelly1PM: reports null descr+type "W" -> add description
+ // Shelly1PM: reports temp senmsors without desc -> add description
+ // Shelly Dimmer: sensors are reported without descriptions -> map to S
+ // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
+ // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
+ // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
+ // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
+ // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
+ if (sen == null) {
+ throw new IllegalArgumentException("sen should not be null!");
+ }
+ if (sen.desc == null) {
+ sen.desc = "";
+ }
+ String desc = sen.desc.toLowerCase();
+
+ // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
+ // we break it up to Power with L:0, Power with L:1...
+ if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
+ String newDesc = substringBefore(sen.desc, "_");
+ String newLink = substringAfter(sen.desc, "_");
+ sen.desc = newDesc;
+ sen.links = newLink;
+ if (!blkMap.containsKey(sen.links)) {
+ // auto-insert a matching blk entry
+ CoIotDescrBlk blk = new CoIotDescrBlk();
+ CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
+ blk.id = sen.links;
+ if (blk0 != null) {
+ blk.desc = blk0.desc + "_" + blk.id;
+ blkMap.put(blk.id, blk);
+ }
+ }
+ }
+
+ switch (sen.type.toLowerCase()) {
+ case "w": // old devices/firmware releases use "W", new ones "P"
+ sen.type = "P";
+ sen.desc = "Power";
+ break;
+ case "tc":
+ sen.type = "T";
+ sen.desc = "Temperature C";
+ break;
+ case "tf":
+ sen.type = "T";
+ sen.desc = "Temperature F";
+ break;
+ case "overtemp":
+ sen.type = "S";
+ sen.desc = "Overtemp";
+ break;
+ case "relay0":
+ case "switch":
+ case "vswitch":
+ sen.type = "S";
+ sen.desc = "State";
+ break;
+ }
+
+ switch (sen.desc.toLowerCase()) {
+ case "motion": // fix acc to spec it's T=M
+ sen.type = "M";
+ sen.desc = "Motion";
+ break;
+ case "battery": // fix: type is B not H
+ sen.type = "B";
+ sen.desc = "Battery";
+ break;
+ case "overtemp":
+ sen.type = "S";
+ sen.desc = "Overtemp";
+ break;
+ case "relay0":
+ case "switch":
+ case "vswitch":
+ sen.type = "S";
+ sen.desc = "State";
+ break;
+ case "e cnt 0 [w-min]": // 4 Pro
+ case "e cnt 1 [w-min]":
+ case "e cnt 2 [w-min]":
+ case "e cnt total [w-min]": // 4 Pro
+ sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
+ break;
+
+ }
+
+ if (sen.desc.isEmpty()) {
+ switch (sen.type.toLowerCase()) {
+ case "p":
+ sen.desc = "Power";
+ break;
+ case "T":
+ sen.desc = "Temperature";
+ break;
+ case "input":
+ sen.type = "S";
+ sen.desc = "Input";
+ break;
+ case "output":
+ sen.type = "S";
+ sen.desc = "Output";
+ break;
+ case "brightness":
+ sen.type = "S";
+ sen.desc = "Brightness";
+ break;
+ case "red":
+ case "green":
+ case "blue":
+ case "white":
+ case "gain":
+ case "temp": // Bulb: Color temperature
+ sen.desc = sen.type;
+ sen.type = "S";
+ break;
+ case "vswitch":
+ // it seems that Shelly tends to break their own spec: T is the description and D is no longer
+ // included -> map D to sen.T and set CatchAll for T
+ sen.desc = sen.type;
+ sen.type = "S";
+ break;
+ // Default: set no description
+ // (there are no T values defined in the CoIoT spec)
+ case "tostate":
+ default:
+ sen.desc = "";
+ }
+ }
+ return sen;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 1
+ *
+ * @author Markus Michels - Initial contribution
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 2
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTVersion2 extends Shelly1CoIoTProtocol implements Shelly1CoIoTInterface {
+ private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTVersion2.class);
+
+ public Shelly1CoIoTVersion2(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+ Map<String, CoIotDescrSen> sensorMap) {
+ super(thingName, thingHandler, blkMap, sensorMap);
+ }
+
+ @Override
+ public int getVersion() {
+ return Shelly1CoapJSonDTO.COIOT_VERSION_2;
+ }
+
+ /**
+ * Process CoIoT status update message. If a status update is received, but the device description has not been
+ * received yet a GET is send to query device description.
+ *
+ * @param sensorUpdates Complete list of sensor updates
+ * @param sen The specific sensor update to handle
+ * @param updates Resulting updates (new updates will be added to input list)
+ */
+ @Override
+ public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+ Map<String, State> updates, ShellyColorUtils col) {
+ // first check the base implementation
+ if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
+ // process by the base class
+ return true;
+ }
+
+ // Process status information and convert into channel updates
+ // Integer rIndex = Integer.parseInt(sen.links) + 1;
+ int rIndex = getIdFromBlk(sen);
+ String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+ : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+ String mGroup = profile.numMeters <= 1 ? CHANNEL_GROUP_METER
+ : CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
+
+ boolean processed = true;
+ double value = getDouble(s.value);
+ String reason = "";
+
+ if (profile.isTRV) {
+ // Special handling for TRV, because it uses duplicate ID values with different meanings
+ switch (sen.id) {
+ case "3101": // current temp
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
+ toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+ break;
+ case "3103": // target temp in C. 4/31, 999=unknown
+ updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
+ toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+ break;
+ case "3116": // S, valveError, 0/1
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_VALVE_ERROR, false);
+ }
+ break;
+ case "3117": // S, mode, 0-5 (0=disabled)
+ value = getDouble(s.value).intValue();
+ updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE, getDecimal(value));
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE, getOnOff(value > 0));
+ break;
+ case "3118": // Valve state
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+ value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+ break;
+ case "3121": // valvePos, Type=S, Range=0/100;
+ boolean updated = updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
+ s.value != -1 ? toQuantityType(getDouble(s.value), 0, Units.PERCENT) : UnDefType.UNDEF);
+ if (updated && s.value >= 0 && s.value != thingHandler.getChannelDouble(CHANNEL_GROUP_CONTROL,
+ CHANNEL_CONTROL_POSITION)) {
+ logger.debug("{}: Valve position changed, force update", thingName);
+ thingHandler.requestUpdates(1, false);
+ }
+ break;
+ default:
+ processed = false;
+ }
+ } else {
+ processed = false;
+ }
+
+ if (processed) {
+ return true;
+ }
+
+ processed = true;
+ switch (sen.id) {
+ case "3106": // L, luminosity, lux, U32, -1
+ case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
+ case "3111": // B, battery, 0-100%, unknown -1
+ case "3112": // S, charger, 0/1
+ case "3115": // S, sensorError, 0/1
+ // processed by base handler
+ break;
+
+ case "6109": // P, overpowerValue, W, U32
+ case "9101":
+ // Relay: S, mode, relay/roller or
+ // Dimmer: S, mode, color/white
+ // skip, could check against thing mode...
+ break;
+
+ case "1101": // relay_0: output, 0/1
+ case "1201": // relay_1: output, 0/1
+ case "1301": // relay_2: output, 0/1
+ case "1401": // relay_3: output, 0/1
+ updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
+ break;
+ case "1102": // roler_0: S, roller, open/close/stop -> roller state
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
+ break;
+ case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
+ int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
+ logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
+ SHELLY_MAX_ROLLER_POS - pos, pos);
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+ toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+ toQuantityType((double) pos, Units.PERCENT));
+ break;
+ case "1105": // Gas: S, valve, closed/opened/not_connected/failure/closing/opening/checking or unknown
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
+ break;
+
+ case "2101": // Input_0: S, input, 0/1
+ case "2201": // Input_1: S, input, 0/1
+ case "2301": // Input_2: S, input, 0/1
+ case "2401": // Input_3: S, input, 0/1
+ handleInput(sen, s, rGroup, updates);
+ break;
+ case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
+ case "2202": // Input_1: EV, inputEvent
+ case "2302": // Input_2: EV, inputEvent
+ case "2402": // Input_3: EV, inputEvent
+ handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
+ break;
+ case "2103": // EVC, inputEventCnt, U16
+ case "2203": // EVC, inputEventCnt, U16
+ case "2303": // EVC, inputEventCnt, U16
+ case "2403": // EVC, inputEventCnt, U16
+ handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
+ break;
+ case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
+ case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
+ case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
+ int idx = getExtTempId(sen.id);
+ if (idx >= 0) {
+ // H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
+ String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
+ // Some devices report values = -999 or 99 during fw update
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
+ toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+ } else {
+ logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
+ }
+ break;
+ case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
+ if ("targetTemp".equalsIgnoreCase(sen.desc)) {
+
+ break; // target temp in F-> ignore
+ }
+ // sensor_0: T, internalTemp, F, 39/88, unknown 999
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
+ toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
+ break;
+ case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
+ case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
+ case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
+ case "3105": // T, deviceTemp, Fahrenheit -40/572
+ // skip, we use only C
+ break;
+
+ case "3107": // C, Gas concentration, U16
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
+ break;
+ case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
+ if (value != -1) {
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+ value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+ } else {
+ logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
+ }
+ break;
+ case "3109": // S, tilt, 0-180deg, -1
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
+ toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
+ break;
+ case "3113": // S, sensorOp, warmup/normal/fault
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
+ break;
+ case "3114": // S, selfTest, not_completed/completed/running/pending
+ updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
+ break;
+ case "3117": // S, extInput, 0/1
+ handleInput(sen, s, rGroup, updates);
+ break;
+ case "3118":
+ updateChannel(updates, mGroup, CHANNEL_SENSOR_VOLTAGE,
+ toQuantityType(getDouble(s.value), 2, Units.VOLT));
+ break;
+
+ case "4101": // relay_0/light_0: P, power, W
+ case "4201": // relay_1/light_1: P, power, W
+ case "4301": // relay_2/light_2: P, power, W
+ case "4401": // relay_3/light_3: P, power, W
+ case "4105": // emeter_0: P, power, W
+ case "4205": // emeter_1: P, power, W
+ case "4305": // emeter_2: P, power, W
+ case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
+ case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
+ updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
+ toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+ if (!profile.isRGBW2 && !profile.isRoller) {
+ // only for regular, not-aggregated meters
+ updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
+ }
+ break;
+
+ case "4103": // relay_0: E, energy, Wmin, U32
+ case "4203": // relay_1: E, energy, Wmin, U32
+ case "4303": // relay_2: E, energy, Wmin, U32
+ case "4403": // relay_3: E, energy, Wmin, U32
+ case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
+ case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
+ case "4106": // emeter_0: E, energy, Wh, U32
+ case "4206": // emeter_1: E, energy, Wh, U32
+ case "4306": // emeter_2: E, energy, Wh, U32
+ double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
+ updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
+ toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+ break;
+
+ case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
+ case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
+ case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
+ updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
+ toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
+ break;
+
+ case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
+ case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
+ case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
+ updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
+ toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
+ break;
+
+ case "4109": // emeter_0: A, current, 0/120A, -1
+ case "4209": // emeter_1: A, current, 0/120A, -1
+ case "4309": // emeter_2: A, current, 0/120A, -1
+ updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
+ toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
+ break;
+
+ case "4110": // emeter_0: S, powerFactor, 0/1, -1
+ case "4210": // emeter_1: S, powerFactor, 0/1, -1
+ case "4310": // emeter_2: S, powerFactor, 0/1, -1
+ updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
+ break;
+
+ case "5101": // {"I":5101,"T":"S","D":"brightness","R":"0/100","L":1},
+ case "5102": // {"I":5102,"T":"S","D":"gain","R":"0/100","L":1},
+ case "5103": // {"I":5103,"T":"S","D":"colorTemp","U":"K","R":"3000/6500","L":1},
+ case "5105": // {"I":5105,"T":"S","D":"red","R":"0/255","L":1},
+ case "5106": // {"I":5106,"T":"S","D":"green","R":"0/255","L":1},
+ case "5107": // {"I":5107,"T":"S","D":"blue","R":"0/255","L":1},
+ case "5108": // {"I":5108,"T":"S","D":"white","R":"0/255","L":1},
+ // already covered by base handler
+ break;
+
+ case "6101": // A, overtemp, 0/1
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+ }
+ break;
+ case "6102": // relay_0: A, overpower, 0/1
+ case "6202": // relay_1: A, overpower, 0/1
+ case "6302": // relay_2: A, overpower, 0/1
+ case "6402": // relay_3: A, overpower, 0/1
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
+ }
+ break;
+ case "6104": // relay_0: A, loadError, 0/1
+ case "6204": // relay_1: A, loadError, 0/1
+ case "6304": // relay_2: A, loadError, 0/1
+ case "6404": // relay_3: A, loadError, 0/1
+ if (s.value == 1) {
+ thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
+ }
+ break;
+ case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
+ reason = getString(s.valueStr);
+ updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
+ if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
+ thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
+ }
+ case "6106": // A, flood, 0/1, -1
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+ value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+
+ case "6107": // A, motion, 0/1, -1
+ // {"I":6107,"T":"A","D":"motion","R":["0/1","-1"],"L":1},
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
+ value == 1 ? OnOffType.ON : OnOffType.OFF);
+ break;
+ case "3119": // Motion timestamp (timestamp os GMT, not adapted to the adapted timezone)
+ // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
+ if (s.value != 0) {
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
+ getTimestamp(getString("GMT"), (long) s.value));
+ }
+ break;
+ case "3120": // motionActive (timestamp os GMT, not adapted to the adapted timezone)
+ // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
+ getTimestamp("GMT", (long) s.value));
+ break;
+
+ case "6108": // A, gas, none/mild/heavy/test or unknown
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
+ break;
+ case "6110": // A, vibration, 0/1, -1=unknown
+ updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+ s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+ if (s.value == 1) {
+ // post event
+ thingHandler.triggerChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM, EVENT_TYPE_VIBRATION);
+ }
+ break;
+ case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
+ if (s.valueArray.size() > 0) {
+ thingHandler.updateWakeupReason(s.valueArray);
+ lastWakeup = (String) s.valueArray.get(0);
+ }
+ break;
+ case "9103": // EVC, cfgChanged, U16
+ if (lastCfgCount == -1 || lastCfgCount != s.value) {
+ thingHandler.requestUpdates(1, true); // refresh config
+ }
+ lastCfgCount = (int) s.value;
+ break;
+
+ default:
+ processed = false;
+ }
+ return processed;
+ }
+
+ @Override
+ public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+ return super.fixDescription(sen, blkMap);
+ }
+
+ private static final String ID_4101_DESCR = "{ \"I\":4101, \"T\":\"P\", \"D\":\"power\", \"U\": \"W\", \"R\":\"0/3500\", \"L\": 1}";
+ private static final String ID_4103_DESCR = "{ \"I\":4103, \"T\":\"E\", \"D\":\"energy\", \"U\": \"Wmin\", \"R\":\"U32\", \"L\": 1}";
+
+ @Override
+ public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
+ if (profile.isDuo && profile.inColor) {
+ addSensor(sensorMap, "4101", ID_4101_DESCR);
+ addSensor(sensorMap, "4103", ID_4103_DESCR);
+ }
+ super.completeMissingSensorDefinition(sensorMap);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.coap.CoAP.Code;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.CoAP.Type;
+import org.eclipse.californium.core.coap.MessageObserverAdapter;
+import org.eclipse.californium.core.coap.Option;
+import org.eclipse.californium.core.coap.OptionNumberRegistry;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDevDescrTypeAdapter;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDevDescription;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotGenericSensorList;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensorTypeAdapter;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link Shelly1CoapHandler} handles the CoIoT/CoAP registration and events.
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoapHandler implements Shelly1CoapListener {
+ private static final byte[] EMPTY_BYTE = new byte[0];
+
+ private final Logger logger = LoggerFactory.getLogger(Shelly1CoapHandler.class);
+ private final ShellyThingInterface thingHandler;
+ private ShellyThingConfiguration config = new ShellyThingConfiguration();
+ private final GsonBuilder gsonBuilder = new GsonBuilder();
+ private final Gson gson;
+ private String thingName;
+
+ private boolean coiotBound = false;
+ private Shelly1CoIoTInterface coiot;
+ private int coiotVers = -1;
+
+ private final Shelly1CoapServer coapServer;
+ private @Nullable CoapClient statusClient;
+ private Request reqDescription = new Request(Code.GET, Type.CON);
+ private Request reqStatus = new Request(Code.GET, Type.CON);
+ private boolean updatesRequested = false;
+ private int coiotPort = COIOT_PORT;
+
+ private long coiotMessages = 0;
+ private long coiotErrors = 0;
+ private int lastSerial = -1;
+ private String lastPayload = "";
+ private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
+ private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
+ private ShellyDeviceProfile profile;
+ private ShellyApiInterface api;
+
+ public Shelly1CoapHandler(ShellyThingInterface thingHandler, Shelly1CoapServer coapServer) {
+ this.thingHandler = thingHandler;
+ this.thingName = thingHandler.getThingName();
+ this.profile = thingHandler.getProfile();
+ this.api = thingHandler.getApi();
+ this.coapServer = coapServer;
+ this.coiot = new Shelly1CoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
+
+ gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
+ gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
+ gson = gsonBuilder.create();
+ }
+
+ /**
+ * Initialize CoAP access, send discovery packet and start Status server
+ *
+ * @parm thingName Thing name derived from Thing Type/hostname
+ * @parm config ShellyThingConfiguration
+ * @thows ShellyApiException
+ */
+ public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
+ try {
+ this.thingName = thingName;
+ this.config = config;
+ this.profile = thingHandler.getProfile();
+ if (isStarted()) {
+ logger.trace("{}: CoAP Listener was already started", thingName);
+ stop();
+ }
+
+ logger.debug("{}: Starting CoAP Listener", thingName);
+ if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
+ String ps = substringAfter(profile.coiotEndpoint, ":");
+ coiotPort = Integer.parseInt(ps);
+ }
+ coapServer.start(config.localIp, coiotPort, this);
+ statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
+ .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
+ @Nullable
+ Endpoint endpoint = null;
+ CoapClient client = statusClient;
+ if (client != null) {
+ endpoint = client.getEndpoint();
+ }
+ if ((endpoint == null) || !endpoint.isStarted()) {
+ logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
+ throw new ShellyApiException("Network initialization failed");
+ }
+
+ discover();
+ } catch (SocketException e) {
+ logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
+ throw new ShellyApiException("Network error", e);
+ } catch (UnknownHostException e) {
+ logger.info("{}: CoAP Exception (Unknown Host)", thingName, e);
+ throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
+ }
+ }
+
+ public boolean isStarted() {
+ return statusClient != null;
+ }
+
+ /**
+ * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
+ *
+ * @param response The Response packet
+ */
+ @Override
+ public void processResponse(@Nullable Response response) {
+ if (response == null) {
+ coiotErrors++;
+ return; // other device instance
+ }
+ ResponseCode code = response.getCode();
+ if (code != ResponseCode.CONTENT) {
+ // error handling
+ logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
+ response.getPayloadString());
+ coiotErrors++;
+ return;
+ }
+
+ List<Option> options = response.getOptions().asSortedList();
+ String ip = response.getSourceContext().getPeerAddress().toString();
+ boolean match = ip.contains(config.deviceIp);
+ if (!match) {
+ // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
+ for (Option opt : options) {
+ if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
+ String devid = opt.getStringValue();
+ if (devid.contains("#")) {
+ // Format: <device type>#<mac address>#<coap version>
+ String macid = substringBetween(devid, "#", "#");
+ if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
+ match = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!match) {
+ // other instance
+ return;
+ }
+
+ String payload = "";
+ String devId = "";
+ String uri = "";
+ int serial = -1;
+ try {
+ coiotMessages++;
+ if (logger.isDebugEnabled()) {
+ logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
+ response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
+ }
+ if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
+ logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
+ coiotErrors++;
+ return;
+ }
+
+ payload = response.getPayloadString();
+ for (Option opt : options) {
+ switch (opt.getNumber()) {
+ case OptionNumberRegistry.URI_PATH:
+ uri = COLOIT_URI_BASE + opt.getStringValue();
+ break;
+ case OptionNumberRegistry.URI_HOST: // ignore
+ break;
+ case OptionNumberRegistry.CONTENT_FORMAT: // ignore
+ break;
+ case COIOT_OPTION_GLOBAL_DEVID:
+ devId = opt.getStringValue();
+ String sVersion = substringAfterLast(devId, "#");
+ int iVersion = Integer.parseInt(sVersion);
+ if (coiotBound && (coiotVers != iVersion)) {
+ logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
+ thingName, coiotVers, iVersion);
+ thingHandler.reinitializeThing();
+ coiotBound = false;
+ }
+ if (!coiotBound) {
+ thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
+ logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
+ if (iVersion == COIOT_VERSION_1) {
+ coiot = new Shelly1CoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
+ } else if (iVersion == COIOT_VERSION_2) {
+ coiot = new Shelly1CoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
+ } else {
+ logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
+ return;
+ }
+ coiotVers = iVersion;
+ coiotBound = true;
+ }
+ break;
+ case COIOT_OPTION_STATUS_VALIDITY:
+ break;
+ case COIOT_OPTION_STATUS_SERIAL:
+ serial = opt.getIntegerValue();
+ break;
+ default:
+ logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
+ opt.getValue());
+ }
+ }
+
+ // If we received a CoAP message successful the thing must be online
+ thingHandler.setThingOnline();
+
+ // The device changes the serial on every update, receiving a message with the same serial is a
+ // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
+ if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
+ || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
+ logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
+ return;
+ }
+
+ // fixed malformed JSON :-(
+ payload = fixJSON(payload);
+
+ try {
+ if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
+ handleDeviceDescription(devId, payload);
+ } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
+ || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
+ handleStatusUpdate(devId, payload, serial);
+ }
+ } catch (ShellyApiException e) {
+ logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
+ coiotErrors++;
+ }
+
+ if (!updatesRequested) {
+ // Observe Status Updates
+ reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
+ updatesRequested = true;
+ }
+ } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
+ logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
+ resetSerial();
+ coiotErrors++;
+ }
+ }
+
+ /**
+ * Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
+ * etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
+ * status updates to the device units and matching the correct thing channel.
+ *
+ * @param devId The device id reported in the CoIoT message.
+ * @param payload Device desciption in JSon format, example:
+ * {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
+ */
+ private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
+ logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
+
+ try {
+ boolean valid = true;
+
+ // Decode Json
+ CoIotDevDescription descr = fromJson(gson, payload, CoIotDevDescription.class);
+ for (int i = 0; i < descr.blk.size(); i++) {
+ CoIotDescrBlk blk = descr.blk.get(i);
+ logger.debug("{}: id={}: {}", thingName, blk.id, blk.desc);
+ if (!blkMap.containsKey(blk.id)) {
+ blkMap.put(blk.id, blk);
+ } else {
+ blkMap.replace(blk.id, blk);
+ }
+ if ((blk.type != null) && !blk.type.isEmpty()) {
+ // in fact it is a sen entry - that's vioaling the Spec
+ logger.trace("{}: fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
+ blk.desc);
+ CoIotDescrSen sen = new CoIotDescrSen();
+ sen.id = blk.id;
+ sen.desc = blk.desc;
+ sen.type = blk.type;
+ sen.range = blk.range;
+ sen.links = blk.links;
+ valid &= addSensor(sen);
+ }
+ }
+
+ // Save to thing properties
+ thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
+
+ logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
+ if (descr.sen != null) {
+ for (int i = 0; i < descr.sen.size(); i++) {
+ valid &= addSensor(descr.sen.get(i));
+ }
+ }
+ coiot.completeMissingSensorDefinition(sensorMap);
+
+ if (!valid) {
+ logger.debug(
+ "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
+ thingName, coiot.getVersion());
+
+ discover();
+ return;
+ }
+ } catch (JsonSyntaxException e) {
+ logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
+ }
+ }
+
+ /**
+ * Add a new sensor to the sensor table
+ *
+ * @param sen CoIotDescrSen of the sensor
+ */
+ private synchronized boolean addSensor(CoIotDescrSen sen) {
+ logger.debug("{}: id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
+ sen.links);
+ // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
+ // We need to make sure that the persisted device description matches,
+ // otherwise the stored one is discarded and a new discovery is triggered
+ // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
+ int vers = coiot.getVersion();
+ if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
+ || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
+ logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
+ return false;
+ }
+
+ try {
+ CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
+ if (!sensorMap.containsKey(fixed.id)) {
+ sensorMap.put(sen.id, fixed);
+ } else {
+ sensorMap.replace(sen.id, fixed);
+ }
+ } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
+ // description is buggy
+ logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
+ }
+
+ return true;
+ }
+
+ /**
+ * Process CoIoT status update message. If a status update is received, but the device description has not been
+ * received yet a GET is send to query device description.
+ *
+ * @param devId device id included in the status packet
+ * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
+ * @param serial Serial for this request. If this the the same as last serial
+ * the update was already sent and processed so this one gets
+ * ignored.
+ * @throws ShellyApiException
+ */
+ private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
+ logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
+ if (blkMap.isEmpty()) {
+ // send discovery packet
+ resetSerial();
+ discover();
+
+ // try to uses description from last initialization
+ String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
+ if (savedDescr.isEmpty()) {
+ logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
+ return;
+ }
+
+ // simulate received device description to create element table
+ logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
+ handleDeviceDescription(devId, savedDescr);
+ }
+
+ // Parse Json,
+ CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
+ if (list.generic == null) {
+ logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
+ return;
+ }
+
+ List<CoIotSensor> sensorUpdates = list.generic;
+ Map<String, State> updates = new TreeMap<String, State>();
+ logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
+ int failed = 0;
+ ShellyColorUtils col = new ShellyColorUtils();
+ for (int i = 0; i < sensorUpdates.size(); i++) {
+ try {
+ CoIotSensor s = sensorUpdates.get(i);
+ CoIotDescrSen sen = sensorMap.get(s.id);
+ if (sen == null) {
+ logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
+ continue;
+ }
+ // find matching sensor definition from device description, use the Link ID as index
+ CoIotDescrBlk element = null;
+ sen = coiot.fixDescription(sen, blkMap);
+ element = blkMap.get(sen.links);
+ if (element == null) {
+ logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
+ sen.id, payload);
+ continue;
+ }
+ logger.trace("{}: Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
+ i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
+ sen.links, element.desc);
+
+ if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
+ logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
+ sen.id, sen.type, sen.desc, s.value, payload);
+ }
+ } catch (NullPointerException | IllegalArgumentException e) {
+ // even the processing of one value failed we continue with the next one (sometimes this is caused by
+ // buggy formats provided by the device
+ logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
+ payload, e);
+ }
+ }
+
+ if (!updates.isEmpty()) {
+ int updated = 0;
+ for (Map.Entry<String, State> u : updates.entrySet()) {
+ String key = u.getKey();
+ updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
+ }
+ if (updated > 0) {
+ logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
+ if (profile.isSensor || profile.isRoller) {
+ // CoAP is currently lacking the lastUpdate info, so we use host timestamp
+ thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
+ }
+ }
+
+ if (profile.isLight && profile.inColor && col.isRgbValid()) {
+ // Update color picker from single values
+ if (col.isRgbValid()) {
+ thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
+ col.toHSB(), false);
+ }
+ }
+
+ if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
+ // Aggregate Meter Data from different Coap updates
+ int i = 1;
+ double totalCurrent = 0.0;
+ double totalKWH = 0.0;
+ boolean updateMeter = false;
+ while (i <= thingHandler.getProfile().numMeters) {
+ String meter = CHANNEL_GROUP_METER + i;
+ double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
+ double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
+ totalCurrent += current >= 0 ? current : 0;
+ totalKWH += total >= 0 ? total : 0;
+ updateMeter |= current >= 0 | total >= 0;
+ i++;
+ }
+ if (updateMeter) {
+ thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
+ toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
+ thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
+ }
+ }
+
+ // Old firmware release are lacking various status values, which are not updated using CoIoT.
+ // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
+ // of the values are available
+ thingHandler.triggerUpdateFromCoap();
+ } else {
+ if (failed == sensorUpdates.size()) {
+ logger.debug("{}: Device description problem detected, re-discover", thingName);
+ coiotBound = false;
+ discover();
+ }
+ }
+
+ // Remember serial, new packets with same serial will be ignored
+ lastSerial = serial;
+ lastPayload = payload;
+ }
+
+ private void discover() {
+ if (coiot.getVersion() >= 2) {
+ {
+ try {
+ // Try to device description using http request (FW 1.10+)
+ String payload = api.getCoIoTDescription();
+ if (!payload.isEmpty()) {
+ logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
+ handleDeviceDescription(thingName, payload);
+ return;
+ }
+ } catch (ShellyApiException e) {
+ // ignore if not supported by device
+ }
+ }
+ }
+ reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
+ }
+
+ /**
+ * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
+ * JsonSyntaxException
+ *
+ * @param json to be checked/fixed
+ */
+ private static String fixJSON(String payload) {
+ String json = payload;
+ json = json.replace("}{", "},{");
+ json = json.replace("][", "],[");
+ json = json.replace("],,[", "],[");
+ return json;
+ }
+
+ /**
+ * Send a new request (Discovery to get Device Description). Before a pending
+ * request will be canceled.
+ *
+ * @param request The current request (this will be canceled an a new one will
+ * be created)
+ * @param ipAddress Device's IP address
+ * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
+ * @param con true: send as CON, false: send as NON
+ * @return new packet
+ */
+ private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
+ if ((request != null) && !request.isCanceled()) {
+ request.cancel();
+ }
+
+ resetSerial();
+ return newRequest(ipAddress, coiotPort, uri, con).send();
+ }
+
+ /**
+ * Allocate a new Request structure. A message observer will be added to get the
+ * callback when a response has been received.
+ *
+ * @param ipAddress IP address of the device
+ * @param uri URI to be addressed
+ * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
+ * @param con true: send as CON, false: send as NON
+ * @return new packet
+ */
+
+ private Request newRequest(String ipAddress, int port, String uri, Type con) {
+ // We need to build our own Request to set an empty Token
+ Request request = new Request(Code.GET, con);
+ request.setURI(completeUrl(ipAddress, port, uri));
+ request.setToken(EMPTY_BYTE);
+ request.addMessageObserver(new MessageObserverAdapter() {
+ @Override
+ public void onResponse(@Nullable Response response) {
+ processResponse(response);
+ }
+
+ @Override
+ public void onCancel() {
+ logger.debug("{}: CoAP Request was canceled", thingName);
+ }
+
+ @Override
+ public void onTimeout() {
+ logger.debug("{}: CoAP Request timed out", thingName);
+ }
+ });
+ return request;
+ }
+
+ /**
+ * Reset serial and payload used to detect duplicate messages, which have to be ignored.
+ * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
+ * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
+ * the payload.
+ */
+ private void resetSerial() {
+ lastSerial = -1;
+ lastPayload = "";
+ }
+
+ public int getVersion() {
+ return coiotVers;
+ }
+
+ /**
+ * Cancel pending requests and shutdown the client
+ */
+ public synchronized void stop() {
+ if (isStarted()) {
+ logger.debug("{}: Stopping CoAP Listener", thingName);
+ coapServer.stop(this);
+ CoapClient cclient = statusClient;
+ if (cclient != null) {
+ cclient.shutdown();
+ statusClient = null;
+ }
+ Request request = reqDescription;
+ if (!request.isCanceled()) {
+ request.cancel();
+ }
+ request = reqStatus;
+ if (!request.isCanceled()) {
+ request.cancel();
+ }
+ }
+ resetSerial();
+ coiotBound = false;
+ }
+
+ public long getMessageCount() {
+ return coiotMessages;
+ }
+
+ public long getErrorCount() {
+ return coiotErrors;
+ }
+
+ public void dispose() {
+ stop();
+ }
+
+ private static String completeUrl(String ipAddress, int port, String uri) {
+ return "coap://" + ipAddress + ":" + port + uri;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * The {@link Shelly1CoapJSonDTO} helps the CoIoT Json into Java objects
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class Shelly1CoapJSonDTO {
+ // Coap
+ public static final int COIOT_VERSION_1 = 1;
+ public static final int COIOT_VERSION_2 = 2;
+
+ public static final int COIOT_PORT = 5683;
+ public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
+
+ public static final String COLOIT_URI_BASE = "/cit/";
+ public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
+ public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
+
+ public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
+ public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
+ public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
+
+ public static final String COIOT_TAG_BLK = "blk";
+ public static final String COIOT_TAG_SEN = "sen";
+ public static final String COIOT_TAG_ACT = "act";
+ public static final String COIOT_TAG_GENERIC = "G";
+
+ public static class CoIotDescrBlk {
+ @SerializedName("I")
+ String id; // ID
+ @SerializedName("D")
+ String desc; // Description
+
+ // Sometimes sen entries are part of the blk array - not conforming the Spec!
+ @SerializedName("T")
+ public String type; // Type
+ @SerializedName("R")
+ public String range; // Range
+ @SerializedName("L")
+ public String links; // Links
+ }
+
+ public static class CoIotDescrSen {
+ @SerializedName("I")
+ String id; // ID
+ @SerializedName("D")
+ String desc = ""; // Description
+ @SerializedName("T")
+ public String type; // Type
+ @SerializedName("R")
+ public String range; // Range
+ @SerializedName("L")
+ public String links; // Links
+ @SerializedName("U")
+ public String unit; // Unit
+ }
+
+ public static class CoIotDescrP {
+ @SerializedName("I")
+ String id; // ID
+ @SerializedName("D")
+ String desc; // Description
+ @SerializedName("R")
+ public String range; // Range
+ }
+
+ public static class CoIotDescrAct {
+ @SerializedName("I")
+ String id; // ID
+ @SerializedName("D")
+ String desc; // Description
+ @SerializedName("L")
+ public String links; // Links
+ @SerializedName("P")
+ public List<CoIotDescrP> pTag; // ?
+ }
+
+ public static class CoIotDevDescription {
+ public List<CoIotDescrBlk> blk;
+ public List<CoIotDescrSen> sen;
+ // public List<CoIotDescrAct> act;
+
+ public CoIotDevDescription() {
+ blk = new ArrayList<>();
+ sen = new ArrayList<>();
+ }
+ }
+
+ public static class CoIotSensor {
+ @SerializedName("index")
+ public String id; // id
+ public double value; // value
+ public String valueStr; // value
+ public List<Object> valueArray;
+ }
+
+ public static class CoIotGenericSensorList {
+ @SerializedName("G")
+ public List<CoIotSensor> generic;
+
+ public CoIotGenericSensorList() {
+ generic = new ArrayList<>();
+ }
+ }
+
+ protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
+ @Override
+ public CoIotDevDescription read(final JsonReader in) throws IOException {
+ CoIotDevDescription descr = new CoIotDevDescription();
+
+ /*
+ * parse JSON like
+ * "blk": [
+ * { "I": 0, "D": "Relay0"},
+ * { "I": 1, "D": "Sensors"} ],
+ * "sen": [
+ * { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
+ * { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
+ * ]
+ */
+ in.beginObject();
+ String name = in.nextName();
+ if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
+ in.beginArray();
+ while (in.hasNext()) {
+ CoIotDescrBlk blk = new CoIotDescrBlk();
+ in.beginObject();
+ while (in.hasNext()) {
+ switch (in.nextName().toUpperCase()) {
+ case "I":
+ blk.id = in.nextString();
+ break;
+ case "D":
+ blk.desc = in.nextString();
+ break;
+ default:
+ // skip data
+ in.nextNull();
+ }
+ }
+ in.endObject();
+ descr.blk.add(blk);
+ }
+ in.endArray();
+ name = in.nextName();
+ }
+
+ if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
+ /*
+ * parse sensor list, e.g.
+ * "sen":[
+ * { "I":111,"T":"Red","R":"0/255","L":0},
+ * { "I":121,"T":"Green","R":"0/255","L":0},
+ * ]
+ */
+ in.beginArray();
+ while (in.hasNext()) {
+ CoIotDescrSen sen = new CoIotDescrSen();
+ in.beginObject();
+ while (in.hasNext()) {
+ String tag = in.nextName();
+ switch (tag.toUpperCase()) {
+ case "I":
+ sen.id = in.nextString();
+ break;
+ case "D":
+ sen.desc = in.nextString();
+ break;
+ case "T":
+ sen.type = in.nextString();
+ break;
+ case "R":
+ JsonToken token = in.peek();
+ if (token == JsonToken.BEGIN_ARRAY) {
+ // must be v2: an array
+ in.beginArray();
+ sen.range = "";
+ while (in.hasNext()) {
+ String value = in.nextString();
+ sen.range += sen.range.isEmpty() ? value : ";" + value;
+ }
+ in.endArray();
+ } else {
+ sen.range = in.nextString();
+ }
+ break;
+ case "L":
+ sen.links = String.valueOf(in.nextInt());
+ break;
+ case "U": // New in CoAPv2: unit"
+ sen.unit = in.nextString();
+ break;
+ default:
+ // skip data
+ in.nextNull();
+ }
+ }
+ in.endObject();
+ descr.sen.add(sen);
+ }
+
+ in.endArray();
+ if (in.hasNext()) {
+ name = in.nextName();
+ }
+ }
+
+ if (name.equalsIgnoreCase(COIOT_TAG_ACT)) {
+ // skip record
+ in.skipValue();
+ }
+
+ in.endObject();
+ return descr;
+ }
+
+ @Override
+ public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
+ out.beginObject();
+ if (descr != null) {
+ out.name(COIOT_TAG_BLK).beginArray();
+ for (int i = 0; i < descr.blk.size(); i++) {
+ CoIotDescrBlk blk = descr.blk.get(i);
+ out.beginArray();
+ out.value(blk.id);
+ out.value(blk.desc);
+ out.endArray();
+ }
+ out.endArray();
+
+ out.name(COIOT_TAG_SEN).beginArray();
+ for (int i = 0; i < descr.sen.size(); i++) {
+ // Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
+ CoIotDescrSen sen = descr.sen.get(i);
+ out.beginArray();
+ out.value(sen.id);
+ out.value(sen.desc);
+ out.value(sen.type);
+ out.value(sen.range);
+ out.value(sen.links);
+ if (sen.unit != null) {
+ out.value(sen.unit);
+ }
+ out.endArray();
+ }
+ out.endArray();
+ }
+ out.endObject();
+ }
+ }
+
+ protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
+ @Override
+ public CoIotGenericSensorList read(final JsonReader in) throws IOException {
+ CoIotGenericSensorList list = new CoIotGenericSensorList();
+
+ in.beginObject();
+ String generic = in.nextName();
+ if (generic.equals(COIOT_TAG_GENERIC)) {
+ in.beginArray();
+ while (in.hasNext()) {
+ CoIotSensor sensor = new CoIotSensor();
+ in.beginArray();
+ in.nextInt(); // alway 0
+ sensor.id = Integer.toString(in.nextInt());
+ JsonToken token = in.peek();
+ if (token == JsonToken.STRING) {
+ // handle as string
+ sensor.valueStr = in.nextString();
+ sensor.value = -1;
+ } else if (token == JsonToken.NUMBER) {
+ // handle as double
+ sensor.value = in.nextDouble();
+ sensor.valueStr = "";
+ } else if (token == JsonToken.BEGIN_ARRAY) {
+ sensor.valueArray = new ArrayList<>();
+ in.beginArray();
+ while (in.hasNext()) {
+ if (in.peek() == JsonToken.STRING) {
+ sensor.valueArray.add(in.nextString());
+ } else {
+ // skip
+ in.nextNull();
+ }
+ }
+ in.endArray();
+ }
+ in.endArray();
+ list.generic.add(sensor);
+ }
+ in.endArray();
+ }
+ in.endObject();
+
+ return list;
+ }
+
+ @Override
+ public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
+ CoIotGenericSensorList sensors = o;
+ out.beginObject();
+ if (sensors != null) {
+ out.name(COIOT_TAG_GENERIC).beginArray();
+ for (int i = 0; i < sensors.generic.size(); i++) {
+ out.beginArray();
+ out.value(0);
+ out.value(sensors.generic.get(i).id);
+ out.value(sensors.generic.get(i).value);
+ out.endArray();
+ }
+ out.endArray();
+ }
+ out.endObject();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link Shelly1CoapListener} describes the listening interface to process Coap responses
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public interface Shelly1CoapListener {
+ public void processResponse(@Nullable Response response);
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.COIOT_PORT;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.coap.CoAP.Code;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.CoapEndpoint;
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.config.NetworkConfig;
+import org.eclipse.californium.elements.UdpMulticastConnector;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoapServer} implements the UDP listener and status event processor (for /cit/s messages)
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoapServer {
+ private final Logger logger = LoggerFactory.getLogger(Shelly1CoapServer.class);
+
+ boolean started = false;
+ private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
+ private @Nullable UdpMulticastConnector statusConnector;
+ private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
+ private final Set<Shelly1CoapListener> coapListeners = ConcurrentHashMap.newKeySet();
+
+ protected class ShellyStatusListener extends CoapResource {
+ private Shelly1CoapServer listener;
+
+ public ShellyStatusListener(String uri, Shelly1CoapServer listener) {
+ super(uri, true);
+ getAttributes().setTitle("ShellyCoapListener");
+ this.listener = listener;
+ }
+
+ @Override
+ public void handleRequest(@Nullable final Exchange exchange) {
+ if (exchange != null) {
+ Request request = exchange.getRequest();
+ Code code = exchange.getRequest().getCode();
+ switch (code) {
+ case CUSTOM_30:
+ case PUT: // Shelly Motion beta: incorrect, but handle the format
+ listener.processResponse(createResponse(request));
+ break;
+ default:
+ super.handleRequest(exchange);
+ }
+ }
+ }
+ }
+
+ public synchronized void start(String localIp, int port, Shelly1CoapListener listener)
+ throws UnknownHostException, SocketException {
+ if (!started) {
+ logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
+ NetworkConfig nc = NetworkConfig.getStandard();
+ InetAddress localAddr = InetAddress.getByName(localIp);
+ InetSocketAddress localPort = new InetSocketAddress(port);
+
+ // Join the multicast group on the selected network interface
+ statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
+ statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
+ server = new CoapServer(NetworkConfig.getStandard(), port);
+ server.addEndpoint(statusEndpoint);
+ CoapResource cit = new ShellyStatusListener("cit", this);
+ CoapResource s = new ShellyStatusListener("s", this);
+ cit.add(s);
+ server.add(cit);
+ started = true;
+ }
+
+ if (!coapListeners.contains(listener)) {
+ coapListeners.add(listener);
+ }
+ }
+
+ protected void processResponse(Response response) {
+ coapListeners.forEach(listener -> listener.processResponse(response));
+ }
+
+ public static Response createResponse(Request request) {
+ Response response = Response.createResponse(request, ResponseCode.CONTENT);
+ response.setType(request.getType());
+ response.setSourceContext(request.getSourceContext());
+ response.setMID(request.getMID());
+ response.setOptions(request.getOptions());
+ response.setPayload(request.getPayload());
+ return response;
+ }
+
+ @Nullable
+ public CoapEndpoint getEndpoint() {
+ return statusEndpoint;
+ }
+
+ /**
+ * Cancel pending requests and shutdown the client
+ */
+ public void stop(Shelly1CoapListener listener) {
+ coapListeners.remove(listener);
+ if (coapListeners.isEmpty()) {
+ stop();
+ }
+ }
+
+ private synchronized void stop() {
+ if (started) {
+ // Last listener
+ server.stop();
+ statusEndpoint.stop();
+ coapListeners.clear();
+ started = false;
+ logger.debug("CoAP Listener stopped");
+ }
+ }
+
+ public void dispose() {
+ stop();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyApiResult;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySendKeyList;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySenseKeyCode;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
+ * (not
+ * cloud api).
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterface {
+ private final Logger logger = LoggerFactory.getLogger(Shelly1HttpApi.class);
+ private final ShellyDeviceProfile profile;
+
+ public Shelly1HttpApi(String thingName, ShellyThingInterface thing) {
+ super(thingName, thing);
+ profile = thing.getProfile();
+ }
+
+ /**
+ * Simple initialization - called by discovery handler
+ *
+ * @param thingName Symbolic thing name
+ * @param config Thing Configuration
+ * @param httpClient HTTP Client to be passed to ShellyHttpClient
+ */
+ public Shelly1HttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+ super(thingName, config, httpClient);
+ this.profile = new ShellyDeviceProfile();
+ }
+
+ @Override
+ public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
+ return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
+ }
+
+ @Override
+ public String setDebug(boolean enabled) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
+ }
+
+ @Override
+ public String getDebugLog(String id) throws ShellyApiException {
+ return callApi("/debug/" + id, String.class);
+ }
+
+ /**
+ * Initialize the device profile
+ *
+ * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
+ * @return Initialized ShellyDeviceProfile
+ * @throws ShellyApiException
+ */
+ @Override
+ public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+ String json = httpRequest(SHELLY_URL_SETTINGS);
+ if (json.contains("\"type\":\"SHDM-")) {
+ logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
+ json = fixDimmerJson(json);
+ }
+
+ // Map settings to device profile for Light and Sense
+ profile.initialize(thingType, json);
+
+ // 2nd level initialization
+ profile.thingName = profile.hostname;
+ if (profile.isLight && (profile.numMeters == 0)) {
+ logger.debug("{}: Get number of meters from light status", thingName);
+ ShellyStatusLight status = getLightStatus();
+ profile.numMeters = status.meters != null ? status.meters.size() : 0;
+ }
+ if (profile.isSense) {
+ profile.irCodes = getIRCodeList();
+ logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
+ }
+
+ return profile;
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return profile.initialized;
+ }
+
+ /**
+ * Get generic device settings/status. Json returned from API will be mapped to a Gson object
+ *
+ * @return Device settings/status as ShellySettingsStatus object
+ * @throws ShellyApiException
+ */
+ @Override
+ public ShellySettingsStatus getStatus() throws ShellyApiException {
+ String json = "";
+ try {
+ json = httpRequest(SHELLY_URL_STATUS);
+ // Dimmer2 returns invalid json type for loaderror :-(
+ json = getString(json.replace("\"loaderror\":0,", "\"loaderror\":false,"));
+ json = getString(json.replace("\"loaderror\":1,", "\"loaderror\":true,"));
+ ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
+ status.json = json;
+ return status;
+ } catch (JsonSyntaxException e) {
+ throw new ShellyApiException("Unable to parse JSON: " + json, e);
+ }
+ }
+
+ @Override
+ public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
+ return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex, ShellyStatusRelay.class);
+ }
+
+ @Override
+ public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
+ callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
+ ShellyShortLightStatus.class);
+ }
+
+ @Override
+ public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
+ return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
+ ShellyShortLightStatus.class);
+ }
+
+ @Override
+ public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
+ String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
+ httpRequest(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness);
+ }
+
+ @Override
+ public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
+ String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex + "/pos";
+ return callApi(uri, ShellyRollerStatus.class);
+ }
+
+ @Override
+ public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
+ httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=" + turnMode);
+ }
+
+ @Override
+ public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
+ httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=to_pos&roller_pos=" + position);
+ }
+
+ @Override
+ public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
+ return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
+ }
+
+ @Override
+ public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
+ ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
+ if (profile.isSense) {
+ // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
+ status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
+ : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
+ .doubleValue();
+ double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
+ .convert(getDouble(status.tmp.value));
+ status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
+ }
+ if ((status.charger == null) && (profile.settings.externalPower != null)) {
+ // SHelly H&T uses external_power, Sense uses charger
+ status.charger = profile.settings.externalPower != 0;
+ }
+ return status;
+ }
+
+ @Override
+ public void setTimer(int index, String timerName, int value) throws ShellyApiException {
+ String type = SHELLY_CLASS_RELAY;
+ if (profile.isRoller) {
+ type = SHELLY_CLASS_ROLLER;
+ } else if (profile.isLight) {
+ type = SHELLY_CLASS_LIGHT;
+ }
+ String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
+ httpRequest(uri);
+ }
+
+ @Override
+ public void setSleepTime(int value) throws ShellyApiException {
+ httpRequest(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
+ }
+
+ @Override
+ public void setValveTemperature(int valveId, int value) throws ShellyApiException {
+ request("/thermostat/" + valveId + "?target_t_enabled=1&target_t=" + value);
+ }
+
+ @Override
+ public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
+ String uri = "/settings/thermostat/" + valveId + "?target_t_enabled=" + (auto ? "1" : "0");
+ if (auto) {
+ uri = uri + "&target_t=" + getDouble(profile.settings.thermostats.get(0).targetTemp.value);
+ }
+ httpRequest(uri); // percentage to open the valve
+ }
+
+ @Override
+ public void setValveProfile(int valveId, int value) throws ShellyApiException {
+ String uri = "/settings/thermostat/" + valveId + "?";
+ request(uri + (value == 0 ? "schedule=0" : "schedule=1&schedule_profile=" + value));
+ }
+
+ @Override
+ public void setValvePosition(int valveId, double value) throws ShellyApiException {
+ request("/thermostat/" + valveId + "?pos=" + value); // percentage to open the valve
+ }
+
+ @Override
+ public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
+ request("/settings/thermostat/" + valveId + "?boost_minutes=" + value);
+ }
+
+ @Override
+ public void startValveBoost(int valveId, int value) throws ShellyApiException {
+ int minutes = value != -1 ? value : getInteger(profile.settings.thermostats.get(0).boostMinutes);
+ httpRequest("/thermostat/0?boost_minutes=" + minutes);
+ }
+
+ @Override
+ public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+ httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
+ }
+
+ public ShellySettingsLight getLightSettings() throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
+ }
+
+ @Override
+ public ShellyStatusLight getLightStatus() throws ShellyApiException {
+ return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
+ }
+
+ public void setLightSetting(String parm, String value) throws ShellyApiException {
+ httpRequest(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
+ }
+
+ @Override
+ public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
+ }
+
+ @Override
+ public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + urlEncode(user) + "&password="
+ + urlEncode(password), ShellySettingsLogin.class);
+ }
+
+ @Override
+ public String getCoIoTDescription() throws ShellyApiException {
+ try {
+ return callApi("/cit/d", String.class);
+ } catch (ShellyApiException e) {
+ if (e.getApiResult().isNotFound()) {
+ return ""; // only supported by FW 1.10+
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
+ }
+
+ @Override
+ public String deviceReboot() throws ShellyApiException {
+ return callApi(SHELLY_URL_RESTART, String.class);
+ }
+
+ @Override
+ public String factoryReset() throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
+ }
+
+ @Override
+ public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
+ return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
+ }
+
+ @Override
+ public String setWiFiRecovery(boolean enable) throws ShellyApiException {
+ return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
+ String.class); // FW 1.10+: Enable auto-restart on WiFi problems
+ }
+
+ @Override
+ public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
+ return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
+ }
+
+ @Override
+ public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
+ return callApi("/sta_cache_reset", String.class);
+ }
+
+ public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
+ return callApi("/ota?" + uri, ShellySettingsUpdate.class);
+ }
+
+ @Override
+ public String setCloud(boolean enabled) throws ShellyApiException {
+ return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
+ }
+
+ /**
+ * Change between White and Color Mode
+ *
+ * @param mode
+ * @throws ShellyApiException
+ */
+ @Override
+ public void setLightMode(String mode) throws ShellyApiException {
+ if (!mode.isEmpty() && !profile.mode.equals(mode)) {
+ setLightSetting(SHELLY_API_MODE, mode);
+ profile.mode = mode;
+ profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
+ }
+ }
+
+ /**
+ * Set a single light parameter
+ *
+ * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
+ * @param parm Name of the parameter (see API spec)
+ * @param value The value
+ * @throws ShellyApiException
+ */
+ @Override
+ public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
+ // Bulb, RGW2: /<color mode>/<light id>?parm?value
+ // Dimmer: /light/<light id>?parm=value
+ httpRequest(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
+ }
+
+ @Override
+ public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
+ String url = getControlUriPrefix(lightIndex) + "?";
+ int i = 0;
+ for (String key : parameters.keySet()) {
+ if (i > 0) {
+ url = url + "&";
+ }
+ url = url + key + "=" + parameters.get(key);
+ i++;
+ }
+ httpRequest(url);
+ }
+
+ /**
+ * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
+ * symbolic key code, which gets
+ * map into a PRONTO code
+ *
+ * @return Map of key codes
+ * @throws ShellyApiException
+ */
+ public Map<String, String> getIRCodeList() throws ShellyApiException {
+ String result = httpRequest(SHELLY_URL_LIST_IR);
+ // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
+ String keyList = substringAfter(result, "[");
+ keyList = substringBeforeLast(keyList, "]");
+ keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
+ keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
+ keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
+ String json = "{\"key_codes\" : [" + keyList + "] }";
+ ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
+ Map<String, String> list = new HashMap<>();
+ for (ShellySenseKeyCode key : codes.keyCodes) {
+ if (key != null) {
+ list.put(key.id, key.name);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Sends a IR key code to the Shelly Sense.
+ *
+ * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
+ * plain or hex64 format
+ *
+ * @throws ShellyApiException
+ * @throws IllegalArgumentException
+ */
+ @Override
+ public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
+ String type = "";
+ if (profile.irCodes.containsKey(keyCode)) {
+ type = SHELLY_IR_CODET_STORED;
+ } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
+ type = SHELLY_IR_CODET_PRONTO;
+ } else {
+ type = SHELLY_IR_CODET_PRONTO_HEX;
+ }
+ String url = SHELLY_URL_SEND_IR + "?type=" + type;
+ if (type.equals(SHELLY_IR_CODET_STORED)) {
+ url = url + "&" + "id=" + keyCode;
+ } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
+ String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
+ url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
+ } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
+ url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
+ }
+ httpRequest(url);
+ }
+
+ public void setSenseSetting(String setting, String value) throws ShellyApiException {
+ httpRequest(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
+ }
+
+ /**
+ * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
+ * redirected to the binding's servlet and act as a trigger to schedule a status update
+ *
+ * @param ShellyApiException
+ * @throws ShellyApiException
+ */
+ @Override
+ public void setActionURLs() throws ShellyApiException {
+ setRelayEvents();
+ setDimmerEvents();
+ setSensorEventUrls();
+ }
+
+ private void setRelayEvents() throws ShellyApiException {
+ if (profile.settings.relays != null) {
+ int num = profile.isRoller ? profile.numRollers : profile.numRelays;
+ for (int i = 0; i < num; i++) {
+ setEventUrls(i);
+ }
+ }
+ }
+
+ private void setDimmerEvents() throws ShellyApiException {
+ if (profile.settings.dimmers != null) {
+ int sz = profile.settings.dimmers.size();
+ for (int i = 0; i < sz; i++) {
+ setEventUrls(i);
+ }
+ } else if (profile.isLight) {
+ setEventUrls(0);
+ }
+ }
+
+ /**
+ * Set sensor Action URLs
+ *
+ * @throws ShellyApiException
+ */
+ private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
+ if (profile.isSensor) {
+ logger.debug("{}: Set Sensor Reporting URL", thingName);
+ setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
+ SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
+ SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
+ SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
+ }
+ }
+
+ /**
+ * Set/delete Relay/Roller/Dimmer Action URLs
+ *
+ * @param index Device Index (0-based)
+ * @throws ShellyApiException
+ */
+ private void setEventUrls(Integer index) throws ShellyApiException {
+ if (profile.isRoller) {
+ setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
+ SHELLY_EVENT_ROLLER_STOP);
+ } else if (profile.isDimmer) {
+ // 2 set of URLs
+ setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
+ SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
+ setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
+ SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
+
+ // Relay output
+ setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+ } else if (profile.hasRelays) {
+ // Standard relays: btn_xxx, out_xxx, short/longpush URLs
+ setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
+ setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
+ setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+ } else if (profile.isLight) {
+ // Duo, Bulb
+ setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+ }
+ }
+
+ private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
+ if (config.localIp.isEmpty()) {
+ throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
+ }
+ for (String eventType : eventTypes) {
+ if (profile.containsEventUrl(eventType)) {
+ // H&T adds the type=xx to report_url itself, so we need to ommit here
+ String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
+ String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
+ String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ + profile.thingName + "/" + eclass + urlParm;
+ String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
+ String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
+ if (!enabled && !profile.settingsJson.contains(testUrl)) {
+ // Don't set URL to null when the current one doesn't point to this OH
+ // Don't interfere with a 3rd party App
+ continue;
+ }
+ if (!profile.settingsJson.contains(testUrl)) {
+ // Current Action URL is != new URL
+ logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
+ httpRequest(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
+ }
+ }
+ }
+ }
+
+ private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
+ throws ShellyApiException {
+ for (String eventType : eventTypes) {
+ if (profile.containsEventUrl(eventType)) {
+ String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+ + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
+ String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
+ String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
+ if (!enabled && !profile.settingsJson.contains(test)) {
+ // Don't set URL to null when the current one doesn't point to this OH
+ // Don't interfere with a 3rd party App
+ continue;
+ }
+ test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
+ if (!profile.settingsJson.contains(test)) {
+ // Current Action URL is != new URL
+ logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
+ httpRequest(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType)
+ + "=" + urlEncode(newUrl));
+ }
+ }
+ }
+ }
+
+ private static String mkEventUrl(String eventType) {
+ return eventType + SHELLY_EVENTURL_SUFFIX;
+ }
+
+ /**
+ * Submit GET request and return response, check for invalid responses
+ *
+ * @param uri: URI (e.g. "/settings")
+ */
+ @Override
+ public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
+ String json = request(uri);
+ return fromJson(gson, json, classOfT);
+ }
+
+ private String request(String uri) throws ShellyApiException {
+ ShellyApiResult apiResult = new ShellyApiResult();
+ int retries = 3;
+ boolean timeout = false;
+ while (retries > 0) {
+ try {
+ apiResult = innerRequest(HttpMethod.GET, uri);
+ if (timeout) {
+ logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
+ apiResult.getUrl());
+ timeoutsRecovered++;
+ }
+ return apiResult.response; // successful
+ } catch (ShellyApiException e) {
+ if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
+ // Sensor in sleep mode or API exception for non-battery device or retry counter expired
+ throw e; // non-timeout exception
+ }
+
+ timeout = true;
+ retries--;
+ timeoutErrors++; // count the retries
+ logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
+ }
+ }
+ throw new ShellyApiException("API Timeout or inconsistent result"); // successful
+ }
+
+ private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
+ Request request = null;
+ String url = "http://" + config.deviceIp + uri;
+ ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
+
+ try {
+ request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ if (!config.userId.isEmpty()) {
+ String value = config.userId + ":" + config.password;
+ request.header(HTTP_HEADER_AUTH,
+ HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
+ }
+ request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
+ logger.trace("{}: HTTP {} for {}", thingName, method, url);
+
+ // Do request and get response
+ ContentResponse contentResponse = request.send();
+ apiResult = new ShellyApiResult(contentResponse);
+ String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
+ logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+
+ // validate response, API errors are reported as Json
+ if (contentResponse.getStatus() != HttpStatus.OK_200) {
+ throw new ShellyApiException(apiResult);
+ }
+ if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
+ && !url.contains("/sta_cache_reset")) {
+ throw new ShellyApiException("Unexpected response: " + response);
+ }
+ } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
+ ShellyApiException ex = new ShellyApiException(apiResult, e);
+ if (!ex.isTimeout()) { // will be handled by the caller
+ logger.trace("{}: API call returned exception", thingName, ex);
+ }
+ throw ex;
+ }
+ return apiResult;
+ }
+
+ @Override
+ public String getControlUriPrefix(Integer id) {
+ String uri = "";
+ if (profile.isLight || profile.isDimmer) {
+ if (profile.isDuo || profile.isDimmer) {
+ // Duo + Dimmer
+ uri = SHELLY_URL_CONTROL_LIGHT;
+ } else {
+ // Bulb + RGBW2
+ uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
+ }
+ } else {
+ // Roller, Relay
+ uri = SHELLY_URL_CONTROL_RELEAY;
+ }
+ uri = uri + "/" + id;
+ return uri;
+ }
+
+ @Override
+ public int getTimeoutErrors() {
+ return timeoutErrors;
+ }
+
+ @Override
+ public int getTimeoutsRecovered() {
+ return timeoutsRecovered;
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.core.types.State;
-
-/**
- * The {@link ShellyCoapListener} describes the listening interface to process Coap responses
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public interface ShellyCoIoTInterface {
- public int getVersion();
-
- public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
-
- public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
-
- public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
- Map<String, State> updates, ShellyColorUtils col);
-
- public String getLastWakeup();
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.OpenClosedType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTProtocol {
- private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
- protected final String thingName;
- protected final ShellyThingInterface thingHandler;
- protected final ShellyDeviceProfile profile;
- protected final ShellyApiInterface api;
- protected final Map<String, CoIotDescrBlk> blkMap;
- protected final Map<String, CoIotDescrSen> sensorMap;
- private final Gson gson = new GsonBuilder().create();
-
- // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
- // between a real update or just a repeated status on periodic updates
- protected int lastCfgCount = -1;
- protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
- protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
- protected String lastWakeup = "";
-
- public ShellyCoIoTProtocol(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
- Map<String, CoIotDescrSen> sensorMap) {
- this.thingName = thingName;
- this.thingHandler = thingHandler;
- this.blkMap = blkMap;
- this.sensorMap = sensorMap;
- this.profile = thingHandler.getProfile();
- this.api = thingHandler.getApi();
- }
-
- protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
- Map<String, State> updates, ShellyColorUtils col) {
- // Process status information and convert into channel updates
- int rIndex = getIdFromBlk(sen);
- String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
- : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
-
- switch (sen.type.toLowerCase()) {
- case "b": // BatteryLevel +
- updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
- toQuantityType(s.value, 0, Units.PERCENT));
- break;
- case "h" /* Humidity */:
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
- toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
- break;
- case "m" /* Motion */:
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
- case "l": // Luminosity +
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
- toQuantityType(s.value, DIGITS_LUX, Units.LUX));
- break;
- case "s": // CatchAll
- switch (sen.desc.toLowerCase()) {
- case "state": // Relay status +
- case "output":
- updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
- break;
- case "input":
- handleInput(sen, s, rGroup, updates);
- break;
- case "brightness":
- // already handled by state/output
- break;
- case "overtemp": // ++
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
- }
- break;
- case "position":
- // work around: Roller reports 101% instead max 100
- double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
- toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
- toQuantityType(pos, Units.PERCENT));
- break;
- case "flood":
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
- case "vibration": // DW with FW1.6.5+
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- if (s.value == 1) {
- thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
- EVENT_TYPE_VIBRATION);
- }
- break;
- case "luminositylevel": // +
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
- break;
- case "charger": // Sense
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
- // RGBW2/Bulb
- case "red":
- col.setRed((int) s.value);
- updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
- ShellyColorUtils.toPercent((int) s.value));
- break;
- case "green":
- col.setGreen((int) s.value);
- updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
- ShellyColorUtils.toPercent((int) s.value));
- break;
- case "blue":
- col.setBlue((int) s.value);
- updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
- ShellyColorUtils.toPercent((int) s.value));
- break;
- case "white":
- col.setWhite((int) s.value);
- updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
- ShellyColorUtils.toPercent((int) s.value));
- break;
- case "gain":
- col.setGain((int) s.value);
- updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
- ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
- break;
- case "sensorerror":
- String sensorError = s.valueStr != null ? getString(s.valueStr) : "" + s.value;
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(sensorError));
- break;
- default:
- // Unknown
- return false;
- }
- break;
-
- default:
- // Unknown type
- return false;
- }
-
- return true;
- }
-
- public static boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
- updates.put(mkChannelId(group, channel), value);
- return true;
- }
-
- protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
- int idx = getSensorNumber(sen.desc, sen.id) - 1;
- String iGroup = profile.getInputGroup(idx);
- String iChannel = CHANNEL_INPUT + profile.getInputSuffix(idx);
- updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
- }
-
- protected void handleInputEvent(CoIotDescrSen sen, String type, int count, int serial, Map<String, State> updates) {
- int idx = getSensorNumber(sen.desc, sen.id) - 1;
- String group = profile.getInputGroup(idx);
- if (count == -1) {
- // event type
- updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(idx), new StringType(type));
- inputEvent[idx] = type;
- } else {
- // event count
- updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(idx), getDecimal(count));
- logger.trace(
- "{}: Check button[{}] for event trigger (isButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
- thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
- lastEventCount[idx]);
- if (profile.inButtonMode(idx) && ((profile.hasBattery && (count == 1))
- || ((lastEventCount[idx] != -1) && (count != lastEventCount[idx])))) {
- if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
- logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
- thingHandler.triggerButton(group, idx, inputEvent[idx]);
- }
- }
- lastEventCount[idx] = count;
- }
- }
-
- /**
- *
- * Handles the combined updated of the brightness channel:
- * brightness$Switch is the OnOffType (power state)
- * brightness&Value is the brightness value
- *
- * @param profile Device profile, required to select the channel group and name
- * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
- * @param id Sensor id from the update
- * @param sen Sensor description from the update
- * @param s New sensor value
- * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
- */
- protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
- CoIotSensor s, List<CoIotSensor> allUpdates) {
- String group = "";
- String channel = CHANNEL_BRIGHTNESS;
- String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
- if (profile.isLight || profile.isDimmer) {
- if (profile.isBulb || profile.inColor) {
- group = CHANNEL_GROUP_LIGHT_CONTROL;
- channel = CHANNEL_LIGHT_POWER;
- } else if (profile.isDuo) {
- group = CHANNEL_GROUP_WHITE_CONTROL;
- } else if (profile.isDimmer) {
- group = CHANNEL_GROUP_RELAY_CONTROL;
- } else if (profile.isRGBW2) {
- checkL = String.valueOf(id); // String.valueOf(id - 1); // id is 1-based, L is 0-based
- group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
- logger.trace("{}: updatePower() for L={}", thingName, checkL);
- }
-
- // We need to update brightness and on/off state at the same time to avoid "flipping brightness slider" in
- // the UI
- double brightness = -1.0;
- double power = -1.0;
- for (CoIotSensor update : allUpdates) {
- CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
- if (!checkL.isEmpty() && !d.links.equals(checkL)) {
- // continue until we find the correct one
- continue;
- }
- if (d.desc.equalsIgnoreCase("brightness")) {
- brightness = update.value;
- } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
- power = update.value;
- }
- }
- if (power != -1) {
- updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
- }
- if (brightness != -1) {
- updateChannel(updates, group, channel + "$Value",
- toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
- }
- } else if (profile.hasRelays) {
- group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
- updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- } else if (profile.isSensor) {
- // Sensor state
- if (profile.isDW) { // Door Window has item type Contact
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
- s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
- } else {
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- }
- }
- }
-
- /**
- * Find index of Input id, which is required to map to channel name
- *
- * @parm sensorDesc D field from sensor update
- * @param sensorId The id from the sensor update
- * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
- */
- protected int getSensorNumber(String sensorDesc, String sensorId) {
- int idx = 0;
- for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
- CoIotDescrSen sen = se.getValue();
- if (sen.desc.equalsIgnoreCase(sensorDesc)) {
- idx++; // iterate from input1..2..n
- }
- if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
- int id = getIdFromBlk(sen);
- if (id != -1) {
- return id;
- }
- }
- if (sen.id.equalsIgnoreCase(sensorId)) {
- return idx;
- }
- }
- logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
- return -1;
- }
-
- protected int getIdFromBlk(CoIotDescrSen sen) {
- int idx = -1;
- CoIotDescrBlk blk = blkMap.get(sen.links);
- if (blk != null) {
- String desc = blk.desc.toLowerCase();
- if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
- || desc.startsWith(SHELLY_CLASS_LIGHT) || desc.startsWith(SHELLY_CLASS_EMETER)) {
- if (desc.contains("_")) { // CoAP v2
- idx = Integer.parseInt(substringAfter(desc, "_"));
- } else { // CoAP v1
- if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
- idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
- }
- if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
- idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
- }
- if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
- idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
- }
- }
- idx = idx + 1; // make it 1-based (sen.L is 0-based)
- }
- }
- return idx;
- }
-
- /**
- *
- * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
- *
- * @param sensorId sensorId to map into a channel index
- * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
- */
- protected int getExtTempId(String sensorId) {
- int idx = 0;
- for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
- CoIotDescrSen sen = se.getValue();
- if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
- || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
- idx++; // iterate from temperature1..2..n
- }
- if (sen.id.equalsIgnoreCase(sensorId)) {
- return idx;
- }
- }
- logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
- return -1;
- }
-
- protected ShellyDeviceProfile getProfile() {
- return profile;
- }
-
- public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
- return sen != null ? sen : new CoIotDescrSen();
- }
-
- public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
- }
-
- protected void addSensor(Map<String, CoIotDescrSen> sensorMap, String key, String json) {
- try {
- if (!sensorMap.containsKey(key)) {
- CoIotDescrSen sen = gson.fromJson(json, CoIotDescrSen.class);
- if (sen != null) {
- sensorMap.put(key, sen);
- }
- }
- } catch (JsonSyntaxException e) {
- // should never happen
- logger.trace("Unable to parse sensor definition: {}", json, e);
- }
- }
-
- public String getLastWakeup() {
- return lastWakeup;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.unit.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTVersion1 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
- private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion1.class);
-
- public ShellyCoIoTVersion1(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
- Map<String, CoIotDescrSen> sensorMap) {
- super(thingName, thingHandler, blkMap, sensorMap);
- }
-
- @Override
- public int getVersion() {
- return ShellyCoapJSonDTO.COIOT_VERSION_1;
- }
-
- /**
- * Process CoIoT status update message. If a status update is received, but the device description has not been
- * received yet a GET is send to query device description.
- *
- * @param devId device id included in the status packet
- * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
- * @param serial Serial for this request. If this the the same as last serial
- * the update was already sent and processed so this one gets
- * ignored.
- */
- @Override
- public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
- Map<String, State> updates, ShellyColorUtils col) {
- // first check the base implementation
- if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
- // process by the base class
- return true;
- }
-
- // Process status information and convert into channel updates
- Integer rIndex = Integer.parseInt(sen.links) + 1;
- String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
- : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
- switch (sen.type.toLowerCase()) {
- case "t": // Temperature +
- Double value = getDouble(s.value);
- switch (sen.desc.toLowerCase()) {
- case "temperature": // Sensor Temp
- if (getString(getProfile().settings.temperatureUnits)
- .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
- value = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(s.value))
- .doubleValue();
- }
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
- toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
- break;
- case "temperature f": // Device Temp -> ignore (we use C only)
- break;
- case "temperature c": // Device Temp in C
- // Device temperature
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
- toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
- break;
- case "external temperature f": // Shelly 1/1PM external temp sensors
- // ignore F, we use C only
- break;
- case "external temperature c": // Shelly 1/1PM external temp sensors
- case "external_temperature":
- int idx = getExtTempId(sen.id);
- if (idx > 0) {
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
- toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
- } else {
- logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
- sen.desc);
- }
- break;
- default:
- logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
- }
- break;
- case "p": // Power/Watt
- // 3EM uses 1-based meter IDs, other 0-based
- String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
- : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
- updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
- toQuantityType(s.value, DIGITS_WATT, Units.WATT));
- updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
- break;
- case "s" /* CatchAll */:
- switch (sen.desc.toLowerCase()) {
- case "overtemp":
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
- }
- break;
- case "energy counter 0 [w-min]":
- updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
- toQuantityType(s.value, DIGITS_WATT, Units.WATT));
- break;
- case "energy counter 1 [w-min]":
- case "energy counter 2 [w-min]":
- // we don't use them
- break;
- case "energy counter total [w-h]": // 3EM reports W/h
- case "energy counter total [w-min]":
- Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
- updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
- toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
- break;
- case "voltage":
- updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
- toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
- break;
- case "current":
- updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
- toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
- break;
- case "pf":
- updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
- toQuantityType(getDecimal(s.value), Units.PERCENT));
- break;
- case "position":
- // work around: Roller reports 101% instead max 100
- double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
- toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
- toQuantityType(pos, Units.PERCENT));
- break;
- case "input event": // Shelly Button 1
- handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
- break;
- case "input event counter": // Shelly Button 1/ix3
- handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
- break;
- case "flood":
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
- case "tilt": // DW with FW1.6.5+ //+
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
- toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
- break;
- case "vibration": // DW with FW1.6.5+
- if (profile.isMotion) {
- // handle as status
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- } else if (s.value == 1) {
- // handle as event
- thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
- EVENT_TYPE_VIBRATION);
- }
- break;
- case "temp": // Shelly Bulb
- case "colortemperature": // Shelly Duo
- updateChannel(updates,
- profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
- CHANNEL_COLOR_TEMP,
- ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
- break;
- case "sensor state": // Shelly Gas
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
- break;
- case "alarm state": // Shelly Gas
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
- getStringType(s.valueStr));
- break;
- case "self-test state":// Shelly Gas
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
- getStringType(s.valueStr));
- break;
- case "concentration":// Shelly Gas
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
- toQuantityType(getDouble(s.value), DIGITS_NONE, Units.PARTS_PER_MILLION));
- break;
- case "sensorerror":
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
- break;
- default:
- // Unknown
- return false;
- }
- break;
-
- default:
- // Unknown type
- return false;
- }
- return true;
- }
-
- /**
- *
- * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
- * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
- * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
- * old mapping stays in to support older firmware releases.
- *
- * @param sen Sensor description received from device
- * @return fixed Sensor description (sen)
- */
- @Override
- public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
- // Shelly1: reports null descr+type "Switch" -> map to S
- // Shelly1PM: reports null descr+type "Overtemp" -> map to O
- // Shelly1PM: reports null descr+type "W" -> add description
- // Shelly1PM: reports temp senmsors without desc -> add description
- // Shelly Dimmer: sensors are reported without descriptions -> map to S
- // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
- // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
- // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
- // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
- // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
- if (sen == null) {
- throw new IllegalArgumentException("sen should not be null!");
- }
- if (sen.desc == null) {
- sen.desc = "";
- }
- String desc = sen.desc.toLowerCase();
-
- // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
- // we break it up to Power with L:0, Power with L:1...
- if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
- String newDesc = substringBefore(sen.desc, "_");
- String newLink = substringAfter(sen.desc, "_");
- sen.desc = newDesc;
- sen.links = newLink;
- if (!blkMap.containsKey(sen.links)) {
- // auto-insert a matching blk entry
- CoIotDescrBlk blk = new CoIotDescrBlk();
- CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
- blk.id = sen.links;
- if (blk0 != null) {
- blk.desc = blk0.desc + "_" + blk.id;
- blkMap.put(blk.id, blk);
- }
- }
- }
-
- switch (sen.type.toLowerCase()) {
- case "w": // old devices/firmware releases use "W", new ones "P"
- sen.type = "P";
- sen.desc = "Power";
- break;
- case "tc":
- sen.type = "T";
- sen.desc = "Temperature C";
- break;
- case "tf":
- sen.type = "T";
- sen.desc = "Temperature F";
- break;
- case "overtemp":
- sen.type = "S";
- sen.desc = "Overtemp";
- break;
- case "relay0":
- case "switch":
- case "vswitch":
- sen.type = "S";
- sen.desc = "State";
- break;
- }
-
- switch (sen.desc.toLowerCase()) {
- case "motion": // fix acc to spec it's T=M
- sen.type = "M";
- sen.desc = "Motion";
- break;
- case "battery": // fix: type is B not H
- sen.type = "B";
- sen.desc = "Battery";
- break;
- case "overtemp":
- sen.type = "S";
- sen.desc = "Overtemp";
- break;
- case "relay0":
- case "switch":
- case "vswitch":
- sen.type = "S";
- sen.desc = "State";
- break;
- case "e cnt 0 [w-min]": // 4 Pro
- case "e cnt 1 [w-min]":
- case "e cnt 2 [w-min]":
- case "e cnt total [w-min]": // 4 Pro
- sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
- break;
-
- }
-
- if (sen.desc.isEmpty()) {
- switch (sen.type.toLowerCase()) {
- case "p":
- sen.desc = "Power";
- break;
- case "T":
- sen.desc = "Temperature";
- break;
- case "input":
- sen.type = "S";
- sen.desc = "Input";
- break;
- case "output":
- sen.type = "S";
- sen.desc = "Output";
- break;
- case "brightness":
- sen.type = "S";
- sen.desc = "Brightness";
- break;
- case "red":
- case "green":
- case "blue":
- case "white":
- case "gain":
- case "temp": // Bulb: Color temperature
- sen.desc = sen.type;
- sen.type = "S";
- break;
- case "vswitch":
- // it seems that Shelly tends to break their own spec: T is the description and D is no longer
- // included -> map D to sen.T and set CatchAll for T
- sen.desc = sen.type;
- sen.type = "S";
- break;
- // Default: set no description
- // (there are no T values defined in the CoIoT spec)
- case "tostate":
- default:
- sen.desc = "";
- }
- }
- return sen;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
- *
- * @author Markus Michels - Initial contribution
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.OpenClosedType;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 2
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
- private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion2.class);
-
- public ShellyCoIoTVersion2(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
- Map<String, CoIotDescrSen> sensorMap) {
- super(thingName, thingHandler, blkMap, sensorMap);
- }
-
- @Override
- public int getVersion() {
- return ShellyCoapJSonDTO.COIOT_VERSION_2;
- }
-
- /**
- * Process CoIoT status update message. If a status update is received, but the device description has not been
- * received yet a GET is send to query device description.
- *
- * @param sensorUpdates Complete list of sensor updates
- * @param sen The specific sensor update to handle
- * @param updates Resulting updates (new updates will be added to input list)
- */
- @Override
- public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
- Map<String, State> updates, ShellyColorUtils col) {
- // first check the base implementation
- if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
- // process by the base class
- return true;
- }
-
- // Process status information and convert into channel updates
- // Integer rIndex = Integer.parseInt(sen.links) + 1;
- int rIndex = getIdFromBlk(sen);
- String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
- : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
- String mGroup = profile.numMeters <= 1 ? CHANNEL_GROUP_METER
- : CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
-
- boolean processed = true;
- double value = getDouble(s.value);
- String reason = "";
-
- if (profile.isTRV) {
- // Special handling for TRV, because it uses duplicate ID values with different meanings
- switch (sen.id) {
- case "3101": // current temp
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
- toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
- break;
- case "3103": // target temp in C. 4/31, 999=unknown
- updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
- toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
- break;
- case "3116": // S, valveError, 0/1
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_VALVE_ERROR, false);
- }
- break;
- case "3117": // S, mode, 0-5 (0=disabled)
- value = getDouble(s.value).intValue();
- updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE, getDecimal(value));
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE, getOnOff(value > 0));
- break;
- case "3118": // Valve state
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
- value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
- break;
- case "3121": // valvePos, Type=S, Range=0/100;
- boolean updated = updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
- s.value != -1 ? toQuantityType(getDouble(s.value), 0, Units.PERCENT) : UnDefType.UNDEF);
- if (updated && s.value >= 0 && s.value != thingHandler.getChannelDouble(CHANNEL_GROUP_CONTROL,
- CHANNEL_CONTROL_POSITION)) {
- logger.debug("{}: Valve position changed, force update", thingName);
- thingHandler.requestUpdates(1, false);
- }
- break;
- default:
- processed = false;
- }
- } else {
- processed = false;
- }
-
- if (processed) {
- return true;
- }
-
- processed = true;
- switch (sen.id) {
- case "3106": // L, luminosity, lux, U32, -1
- case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
- case "3111": // B, battery, 0-100%, unknown -1
- case "3112": // S, charger, 0/1
- case "3115": // S, sensorError, 0/1
- // processed by base handler
- break;
-
- case "6109": // P, overpowerValue, W, U32
- case "9101":
- // Relay: S, mode, relay/roller or
- // Dimmer: S, mode, color/white
- // skip, could check against thing mode...
- break;
-
- case "1101": // relay_0: output, 0/1
- case "1201": // relay_1: output, 0/1
- case "1301": // relay_2: output, 0/1
- case "1401": // relay_3: output, 0/1
- updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
- break;
- case "1102": // roler_0: S, roller, open/close/stop -> roller state
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
- break;
- case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
- int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
- logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
- SHELLY_MAX_ROLLER_POS - pos, pos);
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
- toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
- toQuantityType((double) pos, Units.PERCENT));
- break;
- case "1105": // Gas: S, valve, closed/opened/not_connected/failure/closing/opening/checking or unknown
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
- break;
-
- case "2101": // Input_0: S, input, 0/1
- case "2201": // Input_1: S, input, 0/1
- case "2301": // Input_2: S, input, 0/1
- case "2401": // Input_3: S, input, 0/1
- handleInput(sen, s, rGroup, updates);
- break;
- case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
- case "2202": // Input_1: EV, inputEvent
- case "2302": // Input_2: EV, inputEvent
- case "2402": // Input_3: EV, inputEvent
- handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
- break;
- case "2103": // EVC, inputEventCnt, U16
- case "2203": // EVC, inputEventCnt, U16
- case "2303": // EVC, inputEventCnt, U16
- case "2403": // EVC, inputEventCnt, U16
- handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
- break;
- case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
- case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
- case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
- int idx = getExtTempId(sen.id);
- if (idx >= 0) {
- // H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
- String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
- // Some devices report values = -999 or 99 during fw update
- updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
- toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
- } else {
- logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
- }
- break;
- case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
- if ("targetTemp".equalsIgnoreCase(sen.desc)) {
-
- break; // target temp in F-> ignore
- }
- // sensor_0: T, internalTemp, F, 39/88, unknown 999
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
- toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
- break;
- case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
- case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
- case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
- case "3105": // T, deviceTemp, Fahrenheit -40/572
- // skip, we use only C
- break;
-
- case "3107": // C, Gas concentration, U16
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
- break;
- case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
- if (value != -1) {
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
- value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
- } else {
- logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
- }
- break;
- case "3109": // S, tilt, 0-180deg, -1
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
- toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
- break;
- case "3113": // S, sensorOp, warmup/normal/fault
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
- break;
- case "3114": // S, selfTest, not_completed/completed/running/pending
- updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
- break;
- case "3117": // S, extInput, 0/1
- handleInput(sen, s, rGroup, updates);
- break;
- case "3118":
- updateChannel(updates, mGroup, CHANNEL_SENSOR_VOLTAGE,
- toQuantityType(getDouble(s.value), 2, Units.VOLT));
- break;
-
- case "4101": // relay_0/light_0: P, power, W
- case "4201": // relay_1/light_1: P, power, W
- case "4301": // relay_2/light_2: P, power, W
- case "4401": // relay_3/light_3: P, power, W
- case "4105": // emeter_0: P, power, W
- case "4205": // emeter_1: P, power, W
- case "4305": // emeter_2: P, power, W
- case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
- case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
- updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
- toQuantityType(s.value, DIGITS_WATT, Units.WATT));
- if (!profile.isRGBW2 && !profile.isRoller) {
- // only for regular, not-aggregated meters
- updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
- }
- break;
-
- case "4103": // relay_0: E, energy, Wmin, U32
- case "4203": // relay_1: E, energy, Wmin, U32
- case "4303": // relay_2: E, energy, Wmin, U32
- case "4403": // relay_3: E, energy, Wmin, U32
- case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
- case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
- case "4106": // emeter_0: E, energy, Wh, U32
- case "4206": // emeter_1: E, energy, Wh, U32
- case "4306": // emeter_2: E, energy, Wh, U32
- double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
- updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
- toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
- break;
-
- case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
- case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
- case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
- updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
- toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
- break;
-
- case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
- case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
- case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
- updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
- toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
- break;
-
- case "4109": // emeter_0: A, current, 0/120A, -1
- case "4209": // emeter_1: A, current, 0/120A, -1
- case "4309": // emeter_2: A, current, 0/120A, -1
- updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
- toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
- break;
-
- case "4110": // emeter_0: S, powerFactor, 0/1, -1
- case "4210": // emeter_1: S, powerFactor, 0/1, -1
- case "4310": // emeter_2: S, powerFactor, 0/1, -1
- updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
- break;
-
- case "5101": // {"I":5101,"T":"S","D":"brightness","R":"0/100","L":1},
- case "5102": // {"I":5102,"T":"S","D":"gain","R":"0/100","L":1},
- case "5103": // {"I":5103,"T":"S","D":"colorTemp","U":"K","R":"3000/6500","L":1},
- case "5105": // {"I":5105,"T":"S","D":"red","R":"0/255","L":1},
- case "5106": // {"I":5106,"T":"S","D":"green","R":"0/255","L":1},
- case "5107": // {"I":5107,"T":"S","D":"blue","R":"0/255","L":1},
- case "5108": // {"I":5108,"T":"S","D":"white","R":"0/255","L":1},
- // already covered by base handler
- break;
-
- case "6101": // A, overtemp, 0/1
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
- }
- break;
- case "6102": // relay_0: A, overpower, 0/1
- case "6202": // relay_1: A, overpower, 0/1
- case "6302": // relay_2: A, overpower, 0/1
- case "6402": // relay_3: A, overpower, 0/1
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
- }
- break;
- case "6104": // relay_0: A, loadError, 0/1
- case "6204": // relay_1: A, loadError, 0/1
- case "6304": // relay_2: A, loadError, 0/1
- case "6404": // relay_3: A, loadError, 0/1
- if (s.value == 1) {
- thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
- }
- break;
- case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
- reason = getString(s.valueStr);
- updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
- if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
- thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
- }
- case "6106": // A, flood, 0/1, -1
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
- value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
-
- case "6107": // A, motion, 0/1, -1
- // {"I":6107,"T":"A","D":"motion","R":["0/1","-1"],"L":1},
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
- value == 1 ? OnOffType.ON : OnOffType.OFF);
- break;
- case "3119": // Motion timestamp (timestamp os GMT, not adapted to the adapted timezone)
- // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
- if (s.value != 0) {
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
- getTimestamp(getString("GMT"), (long) s.value));
- }
- break;
- case "3120": // motionActive (timestamp os GMT, not adapted to the adapted timezone)
- // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
- getTimestamp("GMT", (long) s.value));
- break;
-
- case "6108": // A, gas, none/mild/heavy/test or unknown
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
- break;
- case "6110": // A, vibration, 0/1, -1=unknown
- updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
- s.value == 1 ? OnOffType.ON : OnOffType.OFF);
- if (s.value == 1) {
- // post event
- thingHandler.triggerChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM, EVENT_TYPE_VIBRATION);
- }
- break;
- case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
- if (s.valueArray.size() > 0) {
- thingHandler.updateWakeupReason(s.valueArray);
- lastWakeup = (String) s.valueArray.get(0);
- }
- break;
- case "9103": // EVC, cfgChanged, U16
- if (lastCfgCount == -1 || lastCfgCount != s.value) {
- thingHandler.requestUpdates(1, true); // refresh config
- }
- lastCfgCount = (int) s.value;
- break;
-
- default:
- processed = false;
- }
- return processed;
- }
-
- @Override
- public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
- return super.fixDescription(sen, blkMap);
- }
-
- private static final String ID_4101_DESCR = "{ \"I\":4101, \"T\":\"P\", \"D\":\"power\", \"U\": \"W\", \"R\":\"0/3500\", \"L\": 1}";
- private static final String ID_4103_DESCR = "{ \"I\":4103, \"T\":\"E\", \"D\":\"energy\", \"U\": \"Wmin\", \"R\":\"U32\", \"L\": 1}";
-
- @Override
- public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
- if (profile.isDuo && profile.inColor) {
- addSensor(sensorMap, "4101", ID_4101_DESCR);
- addSensor(sensorMap, "4103", ID_4103_DESCR);
- }
- super.completeMissingSensorDefinition(sensorMap);
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.eclipse.californium.core.CoapClient;
-import org.eclipse.californium.core.coap.CoAP.Code;
-import org.eclipse.californium.core.coap.CoAP.ResponseCode;
-import org.eclipse.californium.core.coap.CoAP.Type;
-import org.eclipse.californium.core.coap.MessageObserverAdapter;
-import org.eclipse.californium.core.coap.Option;
-import org.eclipse.californium.core.coap.OptionNumberRegistry;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.californium.core.network.Endpoint;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescription;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotGenericSensorList;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensorTypeAdapter;
-import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events.
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoapHandler implements ShellyCoapListener {
- private static final byte[] EMPTY_BYTE = new byte[0];
-
- private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class);
- private final ShellyThingInterface thingHandler;
- private ShellyThingConfiguration config = new ShellyThingConfiguration();
- private final GsonBuilder gsonBuilder = new GsonBuilder();
- private final Gson gson;
- private String thingName;
-
- private boolean coiotBound = false;
- private ShellyCoIoTInterface coiot;
- private int coiotVers = -1;
-
- private final ShellyCoapServer coapServer;
- private @Nullable CoapClient statusClient;
- private Request reqDescription = new Request(Code.GET, Type.CON);
- private Request reqStatus = new Request(Code.GET, Type.CON);
- private boolean updatesRequested = false;
- private int coiotPort = COIOT_PORT;
-
- private long coiotMessages = 0;
- private long coiotErrors = 0;
- private int lastSerial = -1;
- private String lastPayload = "";
- private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
- private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
- private ShellyDeviceProfile profile;
- private ShellyApiInterface api;
-
- public ShellyCoapHandler(ShellyThingInterface thingHandler, ShellyCoapServer coapServer) {
- this.thingHandler = thingHandler;
- this.thingName = thingHandler.getThingName();
- this.profile = thingHandler.getProfile();
- this.api = thingHandler.getApi();
- this.coapServer = coapServer;
- this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
-
- gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
- gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
- gson = gsonBuilder.create();
- }
-
- /**
- * Initialize CoAP access, send discovery packet and start Status server
- *
- * @parm thingName Thing name derived from Thing Type/hostname
- * @parm config ShellyThingConfiguration
- * @thows ShellyApiException
- */
- public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
- try {
- this.thingName = thingName;
- this.config = config;
- this.profile = thingHandler.getProfile();
- if (isStarted()) {
- logger.trace("{}: CoAP Listener was already started", thingName);
- stop();
- }
-
- logger.debug("{}: Starting CoAP Listener", thingName);
- if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
- String ps = substringAfter(profile.coiotEndpoint, ":");
- coiotPort = Integer.parseInt(ps);
- }
- coapServer.start(config.localIp, coiotPort, this);
- statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
- .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
- @Nullable
- Endpoint endpoint = null;
- CoapClient client = statusClient;
- if (client != null) {
- endpoint = client.getEndpoint();
- }
- if ((endpoint == null) || !endpoint.isStarted()) {
- logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
- throw new ShellyApiException("Network initialization failed");
- }
-
- discover();
- } catch (SocketException e) {
- logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
- throw new ShellyApiException("Network error", e);
- } catch (UnknownHostException e) {
- logger.info("{}: CoAP Exception (Unknown Host)", thingName, e);
- throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
- }
- }
-
- public boolean isStarted() {
- return statusClient != null;
- }
-
- /**
- * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
- *
- * @param response The Response packet
- */
- @Override
- public void processResponse(@Nullable Response response) {
- if (response == null) {
- coiotErrors++;
- return; // other device instance
- }
- ResponseCode code = response.getCode();
- if (code != ResponseCode.CONTENT) {
- // error handling
- logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
- response.getPayloadString());
- coiotErrors++;
- return;
- }
-
- List<Option> options = response.getOptions().asSortedList();
- String ip = response.getSourceContext().getPeerAddress().toString();
- boolean match = ip.contains(config.deviceIp);
- if (!match) {
- // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
- for (Option opt : options) {
- if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
- String devid = opt.getStringValue();
- if (devid.contains("#")) {
- // Format: <device type>#<mac address>#<coap version>
- String macid = substringBetween(devid, "#", "#");
- if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
- match = true;
- break;
- }
- }
- }
- }
- }
- if (!match) {
- // other instance
- return;
- }
-
- String payload = "";
- String devId = "";
- String uri = "";
- int serial = -1;
- try {
- coiotMessages++;
- if (logger.isDebugEnabled()) {
- logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
- response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
- }
- if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
- logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
- coiotErrors++;
- return;
- }
-
- payload = response.getPayloadString();
- for (Option opt : options) {
- switch (opt.getNumber()) {
- case OptionNumberRegistry.URI_PATH:
- uri = COLOIT_URI_BASE + opt.getStringValue();
- break;
- case OptionNumberRegistry.URI_HOST: // ignore
- break;
- case OptionNumberRegistry.CONTENT_FORMAT: // ignore
- break;
- case COIOT_OPTION_GLOBAL_DEVID:
- devId = opt.getStringValue();
- String sVersion = substringAfterLast(devId, "#");
- int iVersion = Integer.parseInt(sVersion);
- if (coiotBound && (coiotVers != iVersion)) {
- logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
- thingName, coiotVers, iVersion);
- thingHandler.reinitializeThing();
- coiotBound = false;
- }
- if (!coiotBound) {
- thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
- logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
- if (iVersion == COIOT_VERSION_1) {
- coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
- } else if (iVersion == COIOT_VERSION_2) {
- coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
- } else {
- logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
- return;
- }
- coiotVers = iVersion;
- coiotBound = true;
- }
- break;
- case COIOT_OPTION_STATUS_VALIDITY:
- break;
- case COIOT_OPTION_STATUS_SERIAL:
- serial = opt.getIntegerValue();
- break;
- default:
- logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
- opt.getValue());
- }
- }
-
- // If we received a CoAP message successful the thing must be online
- thingHandler.setThingOnline();
-
- // The device changes the serial on every update, receiving a message with the same serial is a
- // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
- if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
- || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
- logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
- return;
- }
-
- // fixed malformed JSON :-(
- payload = fixJSON(payload);
-
- try {
- if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
- handleDeviceDescription(devId, payload);
- } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
- || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
- handleStatusUpdate(devId, payload, serial);
- }
- } catch (ShellyApiException e) {
- logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
- coiotErrors++;
- }
-
- if (!updatesRequested) {
- // Observe Status Updates
- reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
- updatesRequested = true;
- }
- } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
- logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
- resetSerial();
- coiotErrors++;
- }
- }
-
- /**
- * Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
- * etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
- * status updates to the device units and matching the correct thing channel.
- *
- * @param devId The device id reported in the CoIoT message.
- * @param payload Device desciption in JSon format, example:
- * {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
- */
- private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
- logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
-
- try {
- boolean valid = true;
-
- // Decode Json
- CoIotDevDescription descr = fromJson(gson, payload, CoIotDevDescription.class);
- for (int i = 0; i < descr.blk.size(); i++) {
- CoIotDescrBlk blk = descr.blk.get(i);
- logger.debug("{}: id={}: {}", thingName, blk.id, blk.desc);
- if (!blkMap.containsKey(blk.id)) {
- blkMap.put(blk.id, blk);
- } else {
- blkMap.replace(blk.id, blk);
- }
- if ((blk.type != null) && !blk.type.isEmpty()) {
- // in fact it is a sen entry - that's vioaling the Spec
- logger.trace("{}: fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
- blk.desc);
- CoIotDescrSen sen = new CoIotDescrSen();
- sen.id = blk.id;
- sen.desc = blk.desc;
- sen.type = blk.type;
- sen.range = blk.range;
- sen.links = blk.links;
- valid &= addSensor(sen);
- }
- }
-
- // Save to thing properties
- thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
-
- logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
- if (descr.sen != null) {
- for (int i = 0; i < descr.sen.size(); i++) {
- valid &= addSensor(descr.sen.get(i));
- }
- }
- coiot.completeMissingSensorDefinition(sensorMap);
-
- if (!valid) {
- logger.debug(
- "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
- thingName, coiot.getVersion());
-
- discover();
- return;
- }
- } catch (JsonSyntaxException e) {
- logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
- } catch (NullPointerException | IllegalArgumentException e) {
- logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
- }
- }
-
- /**
- * Add a new sensor to the sensor table
- *
- * @param sen CoIotDescrSen of the sensor
- */
- private synchronized boolean addSensor(CoIotDescrSen sen) {
- logger.debug("{}: id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
- sen.links);
- // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
- // We need to make sure that the persisted device description matches,
- // otherwise the stored one is discarded and a new discovery is triggered
- // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
- int vers = coiot.getVersion();
- if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
- || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
- logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
- return false;
- }
-
- try {
- CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
- if (!sensorMap.containsKey(fixed.id)) {
- sensorMap.put(sen.id, fixed);
- } else {
- sensorMap.replace(sen.id, fixed);
- }
- } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
- // description is buggy
- logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
- }
-
- return true;
- }
-
- /**
- * Process CoIoT status update message. If a status update is received, but the device description has not been
- * received yet a GET is send to query device description.
- *
- * @param devId device id included in the status packet
- * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
- * @param serial Serial for this request. If this the the same as last serial
- * the update was already sent and processed so this one gets
- * ignored.
- * @throws ShellyApiException
- */
- private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
- logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
- if (blkMap.isEmpty()) {
- // send discovery packet
- resetSerial();
- discover();
-
- // try to uses description from last initialization
- String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
- if (savedDescr.isEmpty()) {
- logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
- return;
- }
-
- // simulate received device description to create element table
- logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
- handleDeviceDescription(devId, savedDescr);
- }
-
- // Parse Json,
- CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
- if (list.generic == null) {
- logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
- return;
- }
-
- List<CoIotSensor> sensorUpdates = list.generic;
- Map<String, State> updates = new TreeMap<String, State>();
- logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
- int failed = 0;
- ShellyColorUtils col = new ShellyColorUtils();
- for (int i = 0; i < sensorUpdates.size(); i++) {
- try {
- CoIotSensor s = sensorUpdates.get(i);
- CoIotDescrSen sen = sensorMap.get(s.id);
- if (sen == null) {
- logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
- continue;
- }
- // find matching sensor definition from device description, use the Link ID as index
- CoIotDescrBlk element = null;
- sen = coiot.fixDescription(sen, blkMap);
- element = blkMap.get(sen.links);
- if (element == null) {
- logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
- sen.id, payload);
- continue;
- }
- logger.trace("{}: Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
- i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
- sen.links, element.desc);
-
- if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
- logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
- sen.id, sen.type, sen.desc, s.value, payload);
- }
- } catch (NullPointerException | IllegalArgumentException e) {
- // even the processing of one value failed we continue with the next one (sometimes this is caused by
- // buggy formats provided by the device
- logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
- payload, e);
- }
- }
-
- if (!updates.isEmpty()) {
- int updated = 0;
- for (Map.Entry<String, State> u : updates.entrySet()) {
- String key = u.getKey();
- updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
- }
- if (updated > 0) {
- logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
- if (profile.isSensor || profile.isRoller) {
- // CoAP is currently lacking the lastUpdate info, so we use host timestamp
- thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
- }
- }
-
- if (profile.isLight && profile.inColor && col.isRgbValid()) {
- // Update color picker from single values
- if (col.isRgbValid()) {
- thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
- col.toHSB(), false);
- }
- }
-
- if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
- // Aggregate Meter Data from different Coap updates
- int i = 1;
- double totalCurrent = 0.0;
- double totalKWH = 0.0;
- boolean updateMeter = false;
- while (i <= thingHandler.getProfile().numMeters) {
- String meter = CHANNEL_GROUP_METER + i;
- double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
- double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
- totalCurrent += current >= 0 ? current : 0;
- totalKWH += total >= 0 ? total : 0;
- updateMeter |= current >= 0 | total >= 0;
- i++;
- }
- if (updateMeter) {
- thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
- toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
- thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
- }
- }
-
- // Old firmware release are lacking various status values, which are not updated using CoIoT.
- // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
- // of the values are available
- thingHandler.triggerUpdateFromCoap();
- } else {
- if (failed == sensorUpdates.size()) {
- logger.debug("{}: Device description problem detected, re-discover", thingName);
- coiotBound = false;
- discover();
- }
- }
-
- // Remember serial, new packets with same serial will be ignored
- lastSerial = serial;
- lastPayload = payload;
- }
-
- private void discover() {
- if (coiot.getVersion() >= 2) {
- {
- try {
- // Try to device description using http request (FW 1.10+)
- String payload = api.getCoIoTDescription();
- if (!payload.isEmpty()) {
- logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
- handleDeviceDescription(thingName, payload);
- return;
- }
- } catch (ShellyApiException e) {
- // ignore if not supported by device
- }
- }
- }
- reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
- }
-
- /**
- * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
- * JsonSyntaxException
- *
- * @param json to be checked/fixed
- */
- private static String fixJSON(String payload) {
- String json = payload;
- json = json.replace("}{", "},{");
- json = json.replace("][", "],[");
- json = json.replace("],,[", "],[");
- return json;
- }
-
- /**
- * Send a new request (Discovery to get Device Description). Before a pending
- * request will be canceled.
- *
- * @param request The current request (this will be canceled an a new one will
- * be created)
- * @param ipAddress Device's IP address
- * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
- * @param con true: send as CON, false: send as NON
- * @return new packet
- */
- private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
- if ((request != null) && !request.isCanceled()) {
- request.cancel();
- }
-
- resetSerial();
- return newRequest(ipAddress, coiotPort, uri, con).send();
- }
-
- /**
- * Allocate a new Request structure. A message observer will be added to get the
- * callback when a response has been received.
- *
- * @param ipAddress IP address of the device
- * @param uri URI to be addressed
- * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
- * @param con true: send as CON, false: send as NON
- * @return new packet
- */
-
- private Request newRequest(String ipAddress, int port, String uri, Type con) {
- // We need to build our own Request to set an empty Token
- Request request = new Request(Code.GET, con);
- request.setURI(completeUrl(ipAddress, port, uri));
- request.setToken(EMPTY_BYTE);
- request.addMessageObserver(new MessageObserverAdapter() {
- @Override
- public void onResponse(@Nullable Response response) {
- processResponse(response);
- }
-
- @Override
- public void onCancel() {
- logger.debug("{}: CoAP Request was canceled", thingName);
- }
-
- @Override
- public void onTimeout() {
- logger.debug("{}: CoAP Request timed out", thingName);
- }
- });
- return request;
- }
-
- /**
- * Reset serial and payload used to detect duplicate messages, which have to be ignored.
- * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
- * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
- * the payload.
- */
- private void resetSerial() {
- lastSerial = -1;
- lastPayload = "";
- }
-
- public int getVersion() {
- return coiotVers;
- }
-
- /**
- * Cancel pending requests and shutdown the client
- */
- public synchronized void stop() {
- if (isStarted()) {
- logger.debug("{}: Stopping CoAP Listener", thingName);
- coapServer.stop(this);
- CoapClient cclient = statusClient;
- if (cclient != null) {
- cclient.shutdown();
- statusClient = null;
- }
- Request request = reqDescription;
- if (!request.isCanceled()) {
- request.cancel();
- }
- request = reqStatus;
- if (!request.isCanceled()) {
- request.cancel();
- }
- }
- resetSerial();
- coiotBound = false;
- }
-
- public long getMessageCount() {
- return coiotMessages;
- }
-
- public long getErrorCount() {
- return coiotErrors;
- }
-
- public void dispose() {
- stop();
- }
-
- private static String completeUrl(String ipAddress, int port, String uri) {
- return "coap://" + ipAddress + ":" + port + uri;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import com.google.gson.TypeAdapter;
-import com.google.gson.annotations.SerializedName;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonToken;
-import com.google.gson.stream.JsonWriter;
-
-/**
- * The {@link ShellyCoapJSonDTO} helps the CoIoT Json into Java objects
- *
- * @author Markus Michels - Initial contribution
- */
-public class ShellyCoapJSonDTO {
- // Coap
- public static final int COIOT_VERSION_1 = 1;
- public static final int COIOT_VERSION_2 = 2;
-
- public static final int COIOT_PORT = 5683;
- public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
-
- public static final String COLOIT_URI_BASE = "/cit/";
- public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
- public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
-
- public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
- public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
- public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
-
- public static final String COIOT_TAG_BLK = "blk";
- public static final String COIOT_TAG_SEN = "sen";
- public static final String COIOT_TAG_ACT = "act";
- public static final String COIOT_TAG_GENERIC = "G";
-
- public static class CoIotDescrBlk {
- @SerializedName("I")
- String id; // ID
- @SerializedName("D")
- String desc; // Description
-
- // Sometimes sen entries are part of the blk array - not conforming the Spec!
- @SerializedName("T")
- public String type; // Type
- @SerializedName("R")
- public String range; // Range
- @SerializedName("L")
- public String links; // Links
- }
-
- public static class CoIotDescrSen {
- @SerializedName("I")
- String id; // ID
- @SerializedName("D")
- String desc = ""; // Description
- @SerializedName("T")
- public String type; // Type
- @SerializedName("R")
- public String range; // Range
- @SerializedName("L")
- public String links; // Links
- @SerializedName("U")
- public String unit; // Unit
- }
-
- public static class CoIotDescrP {
- @SerializedName("I")
- String id; // ID
- @SerializedName("D")
- String desc; // Description
- @SerializedName("R")
- public String range; // Range
- }
-
- public static class CoIotDescrAct {
- @SerializedName("I")
- String id; // ID
- @SerializedName("D")
- String desc; // Description
- @SerializedName("L")
- public String links; // Links
- @SerializedName("P")
- public List<CoIotDescrP> pTag; // ?
- }
-
- public static class CoIotDevDescription {
- public List<CoIotDescrBlk> blk;
- public List<CoIotDescrSen> sen;
- // public List<CoIotDescrAct> act;
-
- public CoIotDevDescription() {
- blk = new ArrayList<>();
- sen = new ArrayList<>();
- }
- }
-
- public static class CoIotSensor {
- @SerializedName("index")
- public String id; // id
- public double value; // value
- public String valueStr; // value
- public List<Object> valueArray;
- }
-
- public static class CoIotGenericSensorList {
- @SerializedName("G")
- public List<CoIotSensor> generic;
-
- public CoIotGenericSensorList() {
- generic = new ArrayList<>();
- }
- }
-
- protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
- @Override
- public CoIotDevDescription read(final JsonReader in) throws IOException {
- CoIotDevDescription descr = new CoIotDevDescription();
-
- /*
- * parse JSON like
- * "blk": [
- * { "I": 0, "D": "Relay0"},
- * { "I": 1, "D": "Sensors"} ],
- * "sen": [
- * { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
- * { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
- * ]
- */
- in.beginObject();
- String name = in.nextName();
- if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
- in.beginArray();
- while (in.hasNext()) {
- CoIotDescrBlk blk = new CoIotDescrBlk();
- in.beginObject();
- while (in.hasNext()) {
- switch (in.nextName().toUpperCase()) {
- case "I":
- blk.id = in.nextString();
- break;
- case "D":
- blk.desc = in.nextString();
- break;
- default:
- // skip data
- in.nextNull();
- }
- }
- in.endObject();
- descr.blk.add(blk);
- }
- in.endArray();
- name = in.nextName();
- }
-
- if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
- /*
- * parse sensor list, e.g.
- * "sen":[
- * { "I":111,"T":"Red","R":"0/255","L":0},
- * { "I":121,"T":"Green","R":"0/255","L":0},
- * ]
- */
- in.beginArray();
- while (in.hasNext()) {
- CoIotDescrSen sen = new CoIotDescrSen();
- in.beginObject();
- while (in.hasNext()) {
- String tag = in.nextName();
- switch (tag.toUpperCase()) {
- case "I":
- sen.id = in.nextString();
- break;
- case "D":
- sen.desc = in.nextString();
- break;
- case "T":
- sen.type = in.nextString();
- break;
- case "R":
- JsonToken token = in.peek();
- if (token == JsonToken.BEGIN_ARRAY) {
- // must be v2: an array
- in.beginArray();
- sen.range = "";
- while (in.hasNext()) {
- String value = in.nextString();
- sen.range += sen.range.isEmpty() ? value : ";" + value;
- }
- in.endArray();
- } else {
- sen.range = in.nextString();
- }
- break;
- case "L":
- sen.links = String.valueOf(in.nextInt());
- break;
- case "U": // New in CoAPv2: unit"
- sen.unit = in.nextString();
- break;
- default:
- // skip data
- in.nextNull();
- }
- }
- in.endObject();
- descr.sen.add(sen);
- }
-
- in.endArray();
- if (in.hasNext()) {
- name = in.nextName();
- }
- }
-
- if (name.equalsIgnoreCase(COIOT_TAG_ACT)) {
- // skip record
- in.skipValue();
- }
-
- in.endObject();
- return descr;
- }
-
- @Override
- public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
- out.beginObject();
- if (descr != null) {
- out.name(COIOT_TAG_BLK).beginArray();
- for (int i = 0; i < descr.blk.size(); i++) {
- CoIotDescrBlk blk = descr.blk.get(i);
- out.beginArray();
- out.value(blk.id);
- out.value(blk.desc);
- out.endArray();
- }
- out.endArray();
-
- out.name(COIOT_TAG_SEN).beginArray();
- for (int i = 0; i < descr.sen.size(); i++) {
- // Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
- CoIotDescrSen sen = descr.sen.get(i);
- out.beginArray();
- out.value(sen.id);
- out.value(sen.desc);
- out.value(sen.type);
- out.value(sen.range);
- out.value(sen.links);
- if (sen.unit != null) {
- out.value(sen.unit);
- }
- out.endArray();
- }
- out.endArray();
- }
- out.endObject();
- }
- }
-
- protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
- @Override
- public CoIotGenericSensorList read(final JsonReader in) throws IOException {
- CoIotGenericSensorList list = new CoIotGenericSensorList();
-
- in.beginObject();
- String generic = in.nextName();
- if (generic.equals(COIOT_TAG_GENERIC)) {
- in.beginArray();
- while (in.hasNext()) {
- CoIotSensor sensor = new CoIotSensor();
- in.beginArray();
- in.nextInt(); // alway 0
- sensor.id = Integer.toString(in.nextInt());
- JsonToken token = in.peek();
- if (token == JsonToken.STRING) {
- // handle as string
- sensor.valueStr = in.nextString();
- sensor.value = -1;
- } else if (token == JsonToken.NUMBER) {
- // handle as double
- sensor.value = in.nextDouble();
- sensor.valueStr = "";
- } else if (token == JsonToken.BEGIN_ARRAY) {
- sensor.valueArray = new ArrayList<>();
- in.beginArray();
- while (in.hasNext()) {
- if (in.peek() == JsonToken.STRING) {
- sensor.valueArray.add(in.nextString());
- } else {
- // skip
- in.nextNull();
- }
- }
- in.endArray();
- }
- in.endArray();
- list.generic.add(sensor);
- }
- in.endArray();
- }
- in.endObject();
-
- return list;
- }
-
- @Override
- public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
- CoIotGenericSensorList sensors = o;
- out.beginObject();
- if (sensors != null) {
- out.name(COIOT_TAG_GENERIC).beginArray();
- for (int i = 0; i < sensors.generic.size(); i++) {
- out.beginArray();
- out.value(0);
- out.value(sensors.generic.get(i).id);
- out.value(sensors.generic.get(i).value);
- out.endArray();
- }
- out.endArray();
- }
- out.endObject();
- }
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The {@link ShellyCoapListener} describes the listening interface to process Coap responses
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public interface ShellyCoapListener {
- public void processResponse(@Nullable Response response);
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.COIOT_PORT;
-
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.eclipse.californium.core.CoapResource;
-import org.eclipse.californium.core.CoapServer;
-import org.eclipse.californium.core.coap.CoAP;
-import org.eclipse.californium.core.coap.CoAP.Code;
-import org.eclipse.californium.core.coap.CoAP.ResponseCode;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.californium.core.network.CoapEndpoint;
-import org.eclipse.californium.core.network.Exchange;
-import org.eclipse.californium.core.network.config.NetworkConfig;
-import org.eclipse.californium.elements.UdpMulticastConnector;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoapServer} implements the UDP listener and status event processor (for /cit/s messages)
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoapServer {
- private final Logger logger = LoggerFactory.getLogger(ShellyCoapServer.class);
-
- boolean started = false;
- private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
- private @Nullable UdpMulticastConnector statusConnector;
- private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
- private final Set<ShellyCoapListener> coapListeners = ConcurrentHashMap.newKeySet();
-
- protected class ShellyStatusListener extends CoapResource {
- private ShellyCoapServer listener;
-
- public ShellyStatusListener(String uri, ShellyCoapServer listener) {
- super(uri, true);
- getAttributes().setTitle("ShellyCoapListener");
- this.listener = listener;
- }
-
- @Override
- public void handleRequest(@Nullable final Exchange exchange) {
- if (exchange != null) {
- Request request = exchange.getRequest();
- Code code = exchange.getRequest().getCode();
- switch (code) {
- case CUSTOM_30:
- case PUT: // Shelly Motion beta: incorrect, but handle the format
- listener.processResponse(createResponse(request));
- break;
- default:
- super.handleRequest(exchange);
- }
- }
- }
- }
-
- public synchronized void start(String localIp, int port, ShellyCoapListener listener)
- throws UnknownHostException, SocketException {
- if (!started) {
- logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
- NetworkConfig nc = NetworkConfig.getStandard();
- InetAddress localAddr = InetAddress.getByName(localIp);
- InetSocketAddress localPort = new InetSocketAddress(port);
-
- // Join the multicast group on the selected network interface
- statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
- statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
- server = new CoapServer(NetworkConfig.getStandard(), port);
- server.addEndpoint(statusEndpoint);
- CoapResource cit = new ShellyStatusListener("cit", this);
- CoapResource s = new ShellyStatusListener("s", this);
- cit.add(s);
- server.add(cit);
- started = true;
- }
-
- if (!coapListeners.contains(listener)) {
- coapListeners.add(listener);
- }
- }
-
- protected void processResponse(Response response) {
- coapListeners.forEach(listener -> listener.processResponse(response));
- }
-
- public static Response createResponse(Request request) {
- Response response = Response.createResponse(request, ResponseCode.CONTENT);
- response.setType(request.getType());
- response.setSourceContext(request.getSourceContext());
- response.setMID(request.getMID());
- response.setOptions(request.getOptions());
- response.setPayload(request.getPayload());
- return response;
- }
-
- @Nullable
- public CoapEndpoint getEndpoint() {
- return statusEndpoint;
- }
-
- /**
- * Cancel pending requests and shutdown the client
- */
- public void stop(ShellyCoapListener listener) {
- coapListeners.remove(listener);
- if (coapListeners.isEmpty()) {
- stop();
- }
- }
-
- private synchronized void stop() {
- if (started) {
- // Last listener
- server.stop();
- statusEndpoint.stop();
- coapListeners.clear();
- started = false;
- logger.debug("CoAP Listener stopped");
- }
- }
-
- public void dispose() {
- stop();
- }
-}
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
config.password = bindingConfig.defaultPassword;
try {
- ShellyHttpApi api = new ShellyHttpApi(name, config, httpClient);
+ Shelly1HttpApi api = new Shelly1HttpApi(name, config, httpClient);
profile = api.getDeviceProfile(thingType);
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
*/
package org.openhab.binding.shelly.internal.discovery;
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.BINDING_ID;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.LinkedHashMap;
*/
@NonNullByDefault
public class ShellyThingCreator {
+ // Device Types
+ public static final String SHELLYDT_1 = "SHSW-1";
+ public static final String SHELLYDT_1PM = "SHSW-PM";
+ public static final String SHELLYDT_1L = "SHSW-L";
+ public static final String SHELLYDT_SHPLG = "SHPLG-1";
+ public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
+ public static final String SHELLYDT_SHPLG_U1 = "SHPLG-U1";
+ public static final String SHELLYDT_SHELLY2 = "SHSW-21";
+ public static final String SHELLYDT_SHELLY25 = "SHSW-25";
+ public static final String SHELLYDT_SHPRO = "SHSW-44";
+ public static final String SHELLYDT_EM = "SHEM";
+ public static final String SHELLYDT_3EM = "SHEM-3";
+ public static final String SHELLYDT_HT = "SHHT-1";
+ public static final String SHELLYDT_DW = "SHDW-1";
+ public static final String SHELLYDT_DW2 = "SHDW-2";
+ public static final String SHELLYDT_SENSE = "SHSEN-1";
+ public static final String SHELLYDT_MOTION = "SHMOS-01";
+ public static final String SHELLYDT_MOTION2 = "SHMOS-02";
+ public static final String SHELLYDT_GAS = "SHGS-1";
+ public static final String SHELLYDT_DIMMER = "SHDM-1";
+ public static final String SHELLYDT_DIMMER2 = "SHDM-2";
+ public static final String SHELLYDT_IX3 = "SHIX3-1";
+ public static final String SHELLYDT_BULB = "SHBLB-1";
+ public static final String SHELLYDT_DUO = "SHBDUO-1";
+ public static final String SHELLYDT_DUORGBW = "SHCB-1";
+ public static final String SHELLYDT_VINTAGE = "SHVIN-1";
+ public static final String SHELLYDT_RGBW2 = "SHRGBW2";
+ public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
+ public static final String SHELLYDT_BUTTON2 = "SHBTN-2";
+ public static final String SHELLYDT_UNI = "SHUNI-1";
+ public static final String SHELLYDT_TRV = "SHTRV-01";
+
+ // Shelly Plus Series
+ public static final String SHELLYDT_PLUS1 = "SNSW-001X16EU";
+ public static final String SHELLYDT_PLUS1PM = "SNSW-001P16EU";
+ public static final String SHELLYDT_PLUS1UL = "SNSW-001X15UL";
+ public static final String SHELLYDT_PLUS1PMUL = "SNSW-001P15UL";
+ public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
+ public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
+ public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
+ public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
+ public static final String SHELLYDT_PLUSI4DC = "SNSN-0D24X";
+ public static final String SHELLYDT_PLUSHT = "SNSN-0013A";
+
+ // Shelly Pro Series
+ public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
+ public static final String SHELLYDT_PRO1_2 = "SPSW-101XE16EU";
+ public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
+ public static final String SHELLYDT_PRO1PM_ = "SPSW-201PE16EU";
+ public static final String SHELLYDT_PRO1PM_2 = "SPSW-101PE16EU";
+ public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
+ public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
+ public static final String SHELLYDT_PRO2_RELAY_2 = "SPSW-102XE16EU-relay";
+ public static final String SHELLYDT_PRO2_ROLLER_2 = "SPSW-102XE16EU-roller";
+ public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
+ public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
+ public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-002PE16EU-relay";
+ public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-002PE16EU-roller";
+ public static final String SHELLYDT_PRO3 = "SPSW-003XE16EU";
+ public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
+ public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
+
+ // Thing names
+ public static final String THING_TYPE_SHELLY1_STR = "shelly1";
+ public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
+ public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
+ public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
+ public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
+ public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
+ public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
+ public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
+ public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
+ public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
+ public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
+ public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
+ public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
+ public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
+ public static final String THING_TYPE_SHELLYPLUGU1_STR = "shellyplugu1"; // Shely Plug US
+ public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
+ public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
+ public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
+ public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
+ public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
+ public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
+ public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
+ public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-color";
+ public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-white";
+ public static final String THING_TYPE_SHELLYDUORGBW_STR = "shellycolorbulb";
+ public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
+ public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
+ public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
+ public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
+ public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
+ public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
+ public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
+ public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
+ public static final String THING_TYPE_SHELLYTRV_STR = "shellytrv";
+ public static final String THING_TYPE_SHELLYMOTION_STR = "shellymotion";
+ public static final String THING_TYPE_SHELLYMOTION2_STR = "shellymotion2";
+ public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
+ public static final String THING_TYPE_SHELLYBUTTON2_STR = "shellybutton2";
+ public static final String THING_TYPE_SHELLYUNI_STR = "shellyuni";
+
+ // Shelly Plus Seriens
+ public static final String THING_TYPE_SHELLYPLUS1_STR = "shellyplus1";
+ public static final String THING_TYPE_SHELLYPLUS1PM_STR = "shellyplus1pm";
+ public static final String THING_TYPE_SHELLYPLUS2PM_RELAY_STR = "shellyplus2pm-relay";
+ public static final String THING_TYPE_SHELLYPLUS2PM_ROLLER_STR = "shellyplus2pm-roller";
+ public static final String THING_TYPE_SHELLYPLUSI4_STR = "shellyplusi4";
+ public static final String THING_TYPE_SHELLYPLUSI4DC_STR = "shellyplusi4dc";
+ public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
+ public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
+
+ // Shelly Pro Series
+ public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
+ public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
+ public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
+ public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
+ public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
+ public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
+ public static final String THING_TYPE_SHELLYPRO3_STR = "shellypro3";
+ public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
+
+ public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
+ public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY1L = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1L_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLY2_RELAY_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLY2_ROLLER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLY25_RELAY_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLY25_ROLLER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUGU1 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUGU1_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYUNI = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYUNI_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYDIMMER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYDIMMER2_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYVINTAGE_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDUORGBW = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYDUORGBW_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYDOORWIN_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYDOORWIN2_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYTRV = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYTRV_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYBUTTON1_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYBUTTON2 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYBUTTON2_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLMOTION = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYMOTION_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYRGBW2_COLOR_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYRGBW2_WHITE_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPROTECTED_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYUNKNOWN_STR);
+
+ // Shelly Plus/Pro
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUS1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUS1_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUS1PM = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUS1PM_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUS2PM_RELAY = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUS2PM_ROLLER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUSI4 = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUSI4_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUSI4DC = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUSI4DC_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUSHT = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUSHT_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPLUSPLUGUS = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPLUSPLUGUS_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPRO1_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO1PM = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO1PM_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO2_RELAY = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO2_RELAY_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO2_ROLLER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO2_ROLLER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_RELAY = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_ROLLER = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPRO3_STR);
+ public static final ThingTypeUID THING_TYPE_SHELLYPRO4PM = new ThingTypeUID(BINDING_ID,
+ THING_TYPE_SHELLYPRO4PM_STR);
+
private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
static {
// mapping by device type id
THING_TYPE_MAPPING.put(SHELLYDT_TRV, THING_TYPE_SHELLYTRV_STR);
THING_TYPE_MAPPING.put(SHELLYDT_MOTION, THING_TYPE_SHELLYMOTION_STR);
+ // Plus Series
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS1, THING_TYPE_SHELLYPLUS1_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PM, THING_TYPE_SHELLYPLUS1PM_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS1UL, THING_TYPE_SHELLYPLUS1_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PMUL, THING_TYPE_SHELLYPLUS1PM_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUSPLUGUS, THING_TYPE_SHELLYPLUSPLUGUS_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4DC, THING_TYPE_SHELLYPLUSI4DC_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PLUSHT, THING_TYPE_SHELLYPLUSHT_STR);
+
+ // Pro Series
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO1, THING_TYPE_SHELLYPRO1_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO1_2, THING_TYPE_SHELLYPRO1_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM, THING_TYPE_SHELLYPRO1PM_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_2, THING_TYPE_SHELLYPRO1PM_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY, THING_TYPE_SHELLYPRO2_RELAY_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_2, THING_TYPE_SHELLYPRO2_RELAY_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER, THING_TYPE_SHELLYPRO2_ROLLER_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER_2, THING_TYPE_SHELLYPRO2_ROLLER_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_2, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_2, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO3, THING_TYPE_SHELLYPRO3_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
+ THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
+
// mapping by thing type
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR);
THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1PM_STR, THING_TYPE_SHELLY1PM_STR);
package org.openhab.binding.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import static org.openhab.binding.shelly.internal.handler.ShellyComponents.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.*;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapHandler;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
public String thingType = "";
protected final ShellyApiInterface api;
+ private final HttpClient httpClient;
+
protected ShellyBindingConfiguration bindingConfig;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
protected ShellyDeviceStats stats = new ShellyDeviceStats();
- private final ShellyCoapHandler coap;
+ private final Shelly1CoapHandler coap;
public boolean autoCoIoT = false;
public final ShellyTranslationProvider messages;
* @param httpPort from httpService
*/
public ShellyBaseHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
- final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+ final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing);
this.bindingConfig = bindingConfig;
this.config = getConfigAs(ShellyThingConfiguration.class);
+ this.httpClient = httpClient;
this.localIP = localIP;
this.localPort = String.valueOf(httpPort);
- this.api = new ShellyHttpApi(thingName, config, httpClient);
+ this.api = new Shelly1HttpApi(thingName, config, httpClient);
- coap = new ShellyCoapHandler(this, coapServer);
+ coap = new Shelly1CoapHandler(this, coapServer);
}
@Override
return config;
}
+ @Override
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
/**
* This routine is called every time the Thing configuration has been changed
*/
if (config.eventsCoIoT && (tmpPrf.settings.coiot != null) && (tmpPrf.settings.coiot.enabled != null)) {
String devpeer = getString(tmpPrf.settings.coiot.peer);
- String ourpeer = config.localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT;
+ String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
if (!tmpPrf.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
try {
api.setCoIoTPeer(ourpeer);
if (isButton) {
triggerButton(group, idx, mapButtonEvent(event));
channel = CHANNEL_BUTTON_TRIGGER + profile.getInputSuffix(idx);
- payload = ShellyApiJsonDTO.mapButtonEvent(event);
+ payload = Shelly1ApiJsonDTO.mapButtonEvent(event);
} else {
logger.debug("{}: Relay button is not in memontary or detached mode, ignore SHORT/LONGPUSH",
thingName);
*/
package org.openhab.binding.shelly.internal.handler;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import java.math.BigDecimal;
package org.openhab.binding.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyADC;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyThermnostat;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyADC;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
package org.openhab.binding.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLightChannel;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
* @param httpPort port of the openHAB HTTP API
*/
public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
- final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+ final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
channelColors = new TreeMap<>();
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.core.thing.Thing;
* @param httpPort port of the openHAB HTTP API
*/
public ShellyProtectedHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
- final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+ final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
package org.openhab.binding.shelly.internal.handler;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
* @param httpPort port of the openHAB HTTP API
*/
public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
- final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+ final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
int httpPort, final HttpClient httpClient) {
super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
}
int position = -1;
if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
- ShellyControlRoller rstatus = api.getRollerStatus(index);
+ ShellyRollerStatus rstatus = api.getRollerStatus(index);
if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
}
}
- private void createRollerChannels(ShellyControlRoller roller) {
+ private void createRollerChannels(ShellyRollerStatus roller) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
}
for (ShellySettingsRoller roller : status.rollers) {
if (roller.isValid) {
- ShellyControlRoller control = api.getRollerStatus(i);
+ ShellyRollerStatus control = api.getRollerStatus(i);
Integer relayIndex = i + 1;
String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
: CHANNEL_GROUP_ROL_CONTROL;
// the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
// maps to a different structure (ShellyShortLight).
Gson gson = new Gson();
- ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
+ ShellySettingsStatus dstatus = fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json),
ShellySettingsStatus.class);
logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
public ShellyThingConfiguration getThingConfig();
+ public HttpClient getHttpClient();
+
public String getProperty(String key);
public void updateProperties(String key, String value);
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.PROPERTY_SERVICE_NAME;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_COIOT_MCAST;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_COIOT_MCAST;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
ShellyThingConfiguration config = getThingConfig(th, properties);
ShellyDeviceProfile profile = th.getProfile();
ShellyApiInterface api = th.getApi();
- new ShellyHttpApi(uid, config, httpClient);
+ new Shelly1HttpApi(uid, config, httpClient);
int refreshTimer = 0;
switch (action) {
String peer = getString(profile.settings.coiot.peer);
boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer);
- String newPeer = mcast ? localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
+ String newPeer = mcast ? localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
String displayPeer = mcast ? newPeer : "Multicast";
if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) {
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
new Thread(() -> { // schedule asynchronous reboot
try {
- ShellyHttpApi api = new ShellyHttpApi(uid, config, httpClient);
+ Shelly1HttpApi api = new Shelly1HttpApi(uid, config, httpClient);
ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
String status = getString(result.status);
logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));
package org.openhab.binding.shelly.internal.manager;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import static org.openhab.core.thing.Thing.*;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
try {
Request request = httpClient.newRequest(url).method(method).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS);
- request.header(HttpHeader.ACCEPT, ShellyHttpApi.CONTENT_TYPE_JSON);
+ request.header(HttpHeader.ACCEPT, ShellyHttpClient.CONTENT_TYPE_JSON);
logger.trace("{}: HTTP {} {}", LOG_PREFIX, method, url);
ContentResponse contentResponse = request.send();
apiResult = new ShellyApiResult(contentResponse);
className = substringAfterLast(getClass().toString(), ".");
this.httpService = httpService;
String localIp = getString(networkAddressService.getPrimaryIpv4HostAddress());
- int localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
+ Integer localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
this.manager = new ShellyManager(configurationAdmin, translationProvider,
httpClientFactory.getCommonHttpClient(), localIp, localPort, handlerFactory);
try {
httpService.registerServlet(SERVLET_URI, this, null, httpService.createDefaultHttpContext());
// Promote Shelly Manager usage
- logger.info("{}", translationProvider.get("status.managerstarted", localIp, localPort + ""));
+ logger.info("{}", translationProvider.get("status.managerstarted", localIp, localPort.toString()));
} catch (NamespaceException | ServletException | IllegalArgumentException e) {
logger.warn("{}: Unable to initialize bindingConfig", className, e);
}
package org.openhab.binding.shelly.internal.provider;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_API_INVTEMP;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_API_INVTEMP;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLightChannel;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
- if (!profile.isSensor && !profile.isIX3 && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
+ if (!profile.isSensor && !profile.isIX && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
// Only some devices report the internal device temp
addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
}
Map<String, Channel> add = new LinkedHashMap<>();
String group = profile.getControlGroup(idx);
- ShellySettingsRelay rs = profile.settings.relays.get(idx);
- ShellyShortStatusRelay rstatus = relay.relays.get(idx);
- boolean timer = rs.hasTimer != null || (rstatus != null && rstatus.hasTimer != null); // Dimmer 1/2 have
- // has_timer under /status
- addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
- addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
- addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
- addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
- addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
+ if (profile.settings.relays != null) {
+ ShellySettingsRelay rs = profile.settings.relays.get(idx);
+ ShellyShortStatusRelay rstatus = relay.relays.get(idx);
+ boolean timer = rs.hasTimer != null || (rstatus != null && rstatus.hasTimer != null); // Dimmer 1/2 have
+ // has_timer under
+ // /status
+ addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
+ addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
+ addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+ addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+ addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
+ }
// Shelly 1/1PM Addon
if (relay.extTemperature != null) {
// Shelly Dimmer has an additional brightness channel
addChannel(thing, add, profile.isDimmer, group, CHANNEL_BRIGHTNESS);
- ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
- addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
- addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+ if (profile.settings.dimmers != null) {
+ ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
+ addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+ addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
- ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
- addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+ ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
+ addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+ }
return add;
}
Map<String, Channel> add = new LinkedHashMap<>();
String group = profile.getControlGroup(idx);
- ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
- // The is no brightness channel in color mode, so we need a power channel
- addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
-
- addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
- addChannel(thing, add, light.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
- addChannel(thing, add, status.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+ if (profile.settings.lights != null) {
+ ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
+ // Create power channel in color mode and brightness channel in white mode
+ addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
+ addChannel(thing, add, !profile.inColor, group, CHANNEL_BRIGHTNESS);
+ addChannel(thing, add, light.temp != null, CHANNEL_GROUP_WHITE_CONTROL, CHANNEL_COLOR_TEMP);
+ addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+ addChannel(thing, add, light.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+ addChannel(thing, add, status.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+ }
return add;
}
return add;
}
- public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyControlRoller roller) {
+ public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyRollerStatus roller) {
Map<String, Channel> add = new LinkedHashMap<>();
addChannel(thing, add, true, CHGR_ROLLER, CHANNEL_ROL_CONTROL_CONTROL);
addChannel(thing, add, true, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE);
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
public static char lastChar(String s) {
return s.length() > 1 ? s.charAt(s.length() - 1) : '*';
}
+
+ public static String sha256(String string) throws ShellyApiException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ final byte[] hashbytes = digest.digest(string.getBytes(StandardCharsets.UTF_8));
+ return bytesToHex(hashbytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new ShellyApiException("SHA256 can't be initialzed", e);
+ }
+ }
+
+ public static String bytesToHex(byte[] bytes) {
+ StringBuilder hexString = new StringBuilder(2 * bytes.length);
+ for (int i = 0; i < bytes.length; i++) {
+ String hex = Integer.toHexString(0xff & bytes[i]);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ }
}
<default>admin</default>
<label>@text/binding.shelly.config.defaultPassword.label</label>
<description>@text/binding.shelly.config.defaultPassword.description</description>
+ <context>password</context>
</parameter>
<parameter name="localIP" type="text">
<label>@text/binding.shelly.config.localIP.label</label>
thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery powered)
thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
+# Plus/Pro devices
+ thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
+ thing-type.shelly.shellyplus1pm.description = Shelly Plus 1PM - Single Relay Switch with Power Meter
+ thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
+ thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
+ thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
+ thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
+ thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
+ thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
+ thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
+ thing-type.shelly.shellypro2-roller.description = Shelly Pro 2 - Roller Control
+ thing-type.shelly.shellypro2pm-relay.description= Shelly Pro 2PM - Dual Relay Switch with Power Meter
+ thing-type.shelly.shellypro2pm-roller.description = Shelly Pro 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
+ thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
+
# thing config - shellydevice
thing-type.config.shelly.deviceIp.label = IP Address
thing-type.config.shelly.deviceIp.description = IP Address of the Shelly device
channel-type.shelly.meterReactive.label = Reactive Energy
channel-type.shelly.meterReactive.description = Instantaneous reactive power in Watts (W)
channel-type.shelly.lastPower1.label = Last Power
-channel-type.shelly.lastPower1.description = Rounded power consumption during last minute
+channel-type.shelly.lastPower1.description = Last power consumption #1 - one rounded minute
channel-type.shelly.meterTotal.label = Total Energy Consumption
channel-type.shelly.meterTotal.description = Total energy consumption in kW/h since the device powered up (resets on restart)
channel-type.shelly.meterReturned.label = Total Returned Energy