From 6937f290b343db5db014cf99f211dabf44729bbf Mon Sep 17 00:00:00 2001 From: Hannes Date: Sat, 1 Jun 2024 17:30:17 +0200 Subject: [PATCH] Shelly 2.5: refactored to use HttpBridge (#2573) * Refactored to use HttpBridge * Fix errors; improve code quality --------- Co-authored-by: Stefan Feilmeier --- .../edge/io/shelly/shelly25/IoShelly25.java | 121 ++++++---------- .../io/shelly/shelly25/IoShelly25Impl.java | 135 +++++++++++------- .../edge/io/shelly/shelly25/ShellyApi.java | 107 -------------- .../shelly/shelly25/IoShelly25ImplTest.java | 4 +- 4 files changed, 132 insertions(+), 235 deletions(-) delete mode 100644 io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/ShellyApi.java diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java index b276675dc5e..01149dde732 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25.java @@ -5,10 +5,8 @@ import io.openems.common.channel.AccessMode; import io.openems.common.channel.Level; import io.openems.common.channel.PersistencePriority; -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; import io.openems.common.types.OpenemsType; import io.openems.edge.common.channel.BooleanDoc; -import io.openems.edge.common.channel.BooleanWriteChannel; import io.openems.edge.common.channel.Doc; import io.openems.edge.common.channel.StateChannel; import io.openems.edge.common.channel.value.Value; @@ -41,6 +39,29 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .accessMode(AccessMode.READ_WRITE) // .persistencePriority(PersistencePriority.HIGH) // .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_1)), + + /** + * Indicates whether the associated Relay is in Overtemp-State. + * + * + */ + RELAY_1_OVERTEMP(Doc.of(Level.WARNING) // + .text("Relay 1 has been switched off due to Overtemperature.")), + /** + * Indicates whether the associated Relay is in Overpower-State. + * + * + */ + RELAY_1_OVERPOWER(Doc.of(Level.WARNING) // + .text("Relay 2 has been switched off due to Overpower.")), /** * Holds writes to Relay Output 2 for debugging. * @@ -64,6 +85,28 @@ public static enum ChannelId implements io.openems.edge.common.channel.ChannelId .accessMode(AccessMode.READ_WRITE) // .persistencePriority(PersistencePriority.HIGH) // .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_2)), + /** + * Indicates whether the associated Relay is in Overtemp-State. + * + * + */ + RELAY_2_OVERTEMP(Doc.of(Level.WARNING) // + .text("Relay 2 has been switched off due to Overtemperature.")), + /** + * Indicates whether the associated Relay is in Overpower-State. + * + * + */ + RELAY_2_OVERPOWER(Doc.of(Level.WARNING) // + .text("Relay 2 has been switched off due to Overpower.")), /** * Slave Communication Failed Fault. * @@ -86,80 +129,6 @@ public Doc doc() { } } - /** - * Gets the Channel for {@link ChannelId#RELAY_1}. - * - * @return the Channel - */ - public default BooleanWriteChannel getRelay1Channel() { - return this.channel(ChannelId.RELAY_1); - } - - /** - * Gets the Relay Output 1. See {@link ChannelId#RELAY_1}. - * - * @return the Channel {@link Value} - */ - public default Value getRelay1() { - return this.getRelay1Channel().value(); - } - - /** - * Internal method to set the 'nextValue' on {@link ChannelId#RELAY_1} Channel. - * - * @param value the next value - */ - public default void _setRelay1(Boolean value) { - this.getRelay1Channel().setNextValue(value); - } - - /** - * Sets the Relay Output 1. See {@link ChannelId#RELAY_1}. - * - * @param value the next write value - * @throws OpenemsNamedException on error - */ - public default void setRelay1(boolean value) throws OpenemsNamedException { - this.getRelay1Channel().setNextWriteValue(value); - } - - /** - * Gets the Channel for {@link ChannelId#RELAY_2}. - * - * @return the Channel - */ - public default BooleanWriteChannel getRelay2Channel() { - return this.channel(ChannelId.RELAY_2); - } - - /** - * Gets the Relay Output 2. See {@link ChannelId#RELAY_2}. - * - * @return the Channel {@link Value} - */ - public default Value getRelay2() { - return this.getRelay2Channel().value(); - } - - /** - * Internal method to set the 'nextValue' on {@link ChannelId#RELAY_2} Channel. - * - * @param value the next value - */ - public default void _setRelay2(Boolean value) { - this.getRelay2Channel().setNextValue(value); - } - - /** - * Sets the Relay Output 2. See {@link ChannelId#RELAY_2}. - * - * @param value the next write value - * @throws OpenemsNamedException on error - */ - public default void setRelay2(boolean value) throws OpenemsNamedException { - this.getRelay2Channel().setNextWriteValue(value); - } - /** * Gets the Channel for {@link ChannelId#SLAVE_COMMUNICATION_FAILED}. * diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25Impl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25Impl.java index aad007289db..d69c0cd2ffd 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25Impl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/IoShelly25Impl.java @@ -1,5 +1,9 @@ package io.openems.edge.io.shelly.shelly25; +import static io.openems.common.utils.JsonUtils.getAsBoolean; +import static io.openems.common.utils.JsonUtils.getAsJsonArray; +import static io.openems.common.utils.JsonUtils.getAsJsonObject; + import java.util.Objects; import org.osgi.service.component.ComponentContext; @@ -7,6 +11,7 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.ConfigurationPolicy; import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; import org.osgi.service.event.propertytypes.EventTopics; @@ -14,10 +19,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonElement; + import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.utils.JsonUtils; +import io.openems.edge.bridge.http.api.BridgeHttp; +import io.openems.edge.bridge.http.api.BridgeHttpFactory; import io.openems.edge.common.channel.BooleanWriteChannel; -import io.openems.edge.common.channel.WriteChannel; import io.openems.edge.common.component.AbstractOpenemsComponent; import io.openems.edge.common.component.OpenemsComponent; import io.openems.edge.common.event.EdgeEventConstants; @@ -27,19 +34,23 @@ @Component(// name = "IO.Shelly.25", // immediate = true, // - configurationPolicy = ConfigurationPolicy.REQUIRE// + configurationPolicy = ConfigurationPolicy.REQUIRE // ) @EventTopics({ // - EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, // EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // }) + public class IoShelly25Impl extends AbstractOpenemsComponent implements IoShelly25, DigitalOutput, OpenemsComponent, EventHandler { private final Logger log = LoggerFactory.getLogger(IoShelly25Impl.class); private final BooleanWriteChannel[] digitalOutputChannels; - private ShellyApi shellyApi = null; + private String baseUrl; + + @Reference + private BridgeHttpFactory httpBridgeFactory; + private BridgeHttp httpBridge; public IoShelly25Impl() { super(// @@ -56,12 +67,18 @@ public IoShelly25Impl() { @Activate private void activate(ComponentContext context, Config config) { super.activate(context, config.id(), config.alias(), config.enabled()); - this.shellyApi = new ShellyApi(config.ip()); + this.baseUrl = "http://" + config.ip(); + this.httpBridge = this.httpBridgeFactory.get(); + + if (this.isEnabled()) { + this.httpBridge.subscribeJsonEveryCycle(this.baseUrl + "/status", this::processHttpResult); + } } - @Override @Deactivate protected void deactivate() { + this.httpBridgeFactory.unget(this.httpBridge); + this.httpBridge = null; super.deactivate(); } @@ -72,22 +89,22 @@ public BooleanWriteChannel[] digitalOutputChannels() { @Override public String debugLog() { + // TODO share code with AbstractKmtronicRelay.debugLog() var b = new StringBuilder(); var i = 1; - for (WriteChannel channel : this.digitalOutputChannels) { + for (var channel : this.digitalOutputChannels) { String valueText; var valueOpt = channel.value().asOptional(); if (valueOpt.isPresent()) { valueText = valueOpt.get() ? "x" : "-"; } else { - valueText = "?"; + valueText = "Unknown"; } - b.append(i + valueText); - - // add space for all but the last - if (++i <= this.digitalOutputChannels.length) { - b.append(" "); + b.append(valueText); + if (i < this.digitalOutputChannels.length) { + b.append("|"); } + i++; } return b.toString(); } @@ -99,68 +116,84 @@ public void handleEvent(Event event) { } switch (event.getTopic()) { - case EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE: - this.eventBeforeProcessImage(); - break; + case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // + -> this.eventExecuteWrite(); + } + } - case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE: - this.eventExecuteWrite(); - break; + private record RelayState(Boolean relayIsOn, Boolean overtemp, Boolean overpower) { + private static RelayState from(JsonElement relay) throws OpenemsNamedException { + var relayIsOn = getAsBoolean(relay, "ison"); + var overtemp = getAsBoolean(relay, "overtemperature"); + var overpower = getAsBoolean(relay, "overpower"); + return new RelayState(relayIsOn, overtemp, overpower); + } + + private void applyChannels(IoShelly25 component, IoShelly25.ChannelId relayChannel, + IoShelly25.ChannelId overtempChannel, IoShelly25.ChannelId overpowerChannel) { + component.channel(relayChannel).setNextValue(this.relayIsOn); + component.channel(overtempChannel).setNextValue(this.overtemp); + component.channel(overpowerChannel).setNextValue(this.overpower); } } /** * Execute on Cycle Event "Before Process Image". + * + * @param result The JSON element containing the result of the HTTP request. + * @param error The throwable error, if any occurred during the HTTP request. + * @throws OpenemsNamedException if the processing of the HTTP result fails or + * communication with the slave device is + * unsuccessful. */ - private void eventBeforeProcessImage() { - Boolean relay1IsOn; - Boolean relay2IsOn; - try { - var json = this.shellyApi.getStatus(); - var relays = JsonUtils.getAsJsonArray(json, "relays"); - var relay1 = JsonUtils.getAsJsonObject(relays.get(0)); - relay1IsOn = JsonUtils.getAsBoolean(relay1, "ison"); - var relay2 = JsonUtils.getAsJsonObject(relays.get(1)); - relay2IsOn = JsonUtils.getAsBoolean(relay2, "ison"); + private void processHttpResult(JsonElement result, Throwable error) { + var slaveCommunicationFailed = result == null; + var relay1State = new RelayState(null, null, null); + var relay2State = new RelayState(null, null, null); - this._setSlaveCommunicationFailed(false); + try { + final var relays = getAsJsonArray(result, "relays"); + relay1State = RelayState.from(getAsJsonObject(relays.get(0))); + relay2State = RelayState.from(getAsJsonObject(relays.get(1))); } catch (OpenemsNamedException | IndexOutOfBoundsException e) { - relay1IsOn = null; - relay2IsOn = null; - this.logError(this.log, "Unable to read from Shelly API: " + e.getMessage()); - this._setSlaveCommunicationFailed(true); + this.logDebug(this.log, e.getMessage()); + slaveCommunicationFailed = true; } - this._setRelay1(relay1IsOn); - this._setRelay2(relay2IsOn); + + this._setSlaveCommunicationFailed(slaveCommunicationFailed); + relay1State.applyChannels(this, IoShelly25.ChannelId.RELAY_1, // + IoShelly25.ChannelId.RELAY_1_OVERTEMP, IoShelly25.ChannelId.RELAY_1_OVERPOWER); + relay2State.applyChannels(this, IoShelly25.ChannelId.RELAY_2, // + IoShelly25.ChannelId.RELAY_2_OVERTEMP, IoShelly25.ChannelId.RELAY_2_OVERPOWER); } /** * Execute on Cycle Event "Execute Write". */ private void eventExecuteWrite() { - try { - this.executeWrite(this.getRelay1Channel(), 0); - this.executeWrite(this.getRelay2Channel(), 1); - - this._setSlaveCommunicationFailed(false); - } catch (OpenemsNamedException e) { - this._setSlaveCommunicationFailed(true); + for (int i = 0; i < this.digitalOutputChannels.length; i++) { + this.executeWrite(this.digitalOutputChannels[i], i); } } - private void executeWrite(BooleanWriteChannel channel, int index) throws OpenemsNamedException { + private void executeWrite(BooleanWriteChannel channel, int index) { var readValue = channel.value().get(); var writeValue = channel.getNextWriteValueAndReset(); - if (!writeValue.isPresent()) { - // no write value + if (writeValue.isEmpty()) { return; } if (Objects.equals(readValue, writeValue.get())) { - // read value = write value return; } - this.shellyApi.setRelayTurn(index, writeValue.get()); + final String url = this.baseUrl + "/relay/" + index + "?turn=" + (writeValue.get() ? "on" : "off"); + this.httpBridge.get(url).whenComplete((t, e) -> { + this._setSlaveCommunicationFailed(e != null); + if (e == null) { + this.logInfo(this.log, "Executed write successfully for URL: " + url); + } else { + this.logError(this.log, "Failed to execute write for URL: " + url + "; Error: " + e.getMessage()); + } + }); } - -} \ No newline at end of file +} diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/ShellyApi.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/ShellyApi.java deleted file mode 100644 index 6b3b8a3db6f..00000000000 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shelly25/ShellyApi.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.openems.edge.io.shelly.shelly25; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import io.openems.common.exceptions.OpenemsError.OpenemsNamedException; -import io.openems.common.exceptions.OpenemsException; -import io.openems.common.utils.JsonUtils; - -// TODO replace with HttpBridge -/** - * Implements the local Shelly REST Api. - * - *

- * See https://shelly-api-docs.shelly.cloud - */ -public class ShellyApi { - - private final String baseUrl; - - public ShellyApi(String ip) { - this.baseUrl = "http://" + ip; - } - - /** - * Gets the status of the device. - * - *

- * See https://shelly-api-docs.shelly.cloud/#shelly2-5-status - * - * @return the status as JsonObject according to Shelly docs - * @throws OpenemsNamedException on error - */ - public JsonObject getStatus() throws OpenemsNamedException { - return JsonUtils.getAsJsonObject(this.sendGetRequest("/status")); - } - - /** - * Gets the "ison" state of the relay with the given index. - * - *

- * See https://shelly-api-docs.shelly.cloud/#shelly2-5-relay-index - * - * @param index the index of the relay - * @return the boolean value - * @throws OpenemsNamedException on error - */ - public boolean getRelayIson(int index) throws OpenemsNamedException { - var json = this.sendGetRequest("/relay/" + index); - return JsonUtils.getAsBoolean(json, "ison"); - } - - /** - * Turns the relay with the given index on or off. - * - * @param index the index of the relay - * @param value true to turn on; false to turn off - * @throws OpenemsNamedException on error - */ - public void setRelayTurn(int index, boolean value) throws OpenemsNamedException { - this.sendGetRequest("/relay/" + index + "?turn=" + (value ? "on" : "off")); - } - - /** - * Sends a get request to the Shelly API. - * - * @param endpoint the REST Api endpoint - * @return a JsonObject or JsonArray - * @throws OpenemsNamedException on error - */ - private JsonElement sendGetRequest(String endpoint) throws OpenemsNamedException { - try { - var url = new URL(this.baseUrl + endpoint); - var con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("GET"); - con.setConnectTimeout(5000); - con.setReadTimeout(5000); - var status = con.getResponseCode(); - String body; - try (var in = new BufferedReader(new InputStreamReader(con.getInputStream()))) { - // Read HTTP response - var content = new StringBuilder(); - String line; - while ((line = in.readLine()) != null) { - content.append(line); - content.append(System.lineSeparator()); - } - body = content.toString(); - } - if (status < 300) { - // Parse response to JSON - return JsonUtils.parseToJsonObject(body); - } - throw new OpenemsException("Error while reading from Shelly API. Response code: " + status + ". " + body); - } catch (OpenemsNamedException | IOException e) { - throw new OpenemsException( - "Unable to read from Shelly API. " + e.getClass().getSimpleName() + ": " + e.getMessage()); - } - } - -} diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java index 6e778d2393c..2e6e2a98902 100644 --- a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shelly25/IoShelly25ImplTest.java @@ -2,6 +2,7 @@ import org.junit.Test; +import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; import io.openems.edge.common.test.ComponentTest; public class IoShelly25ImplTest { @@ -11,6 +12,7 @@ public class IoShelly25ImplTest { @Test public void test() throws Exception { new ComponentTest(new IoShelly25Impl()) // + .addReference("httpBridgeFactory", new DummyBridgeHttpFactory()) // .activate(MyConfig.create() // .setId(COMPONENT_ID) // .setIp("127.0.0.1") // @@ -18,4 +20,4 @@ public void test() throws Exception { ; } -} +} \ No newline at end of file