diff --git a/io.openems.edge.io.shelly/readme.adoc b/io.openems.edge.io.shelly/readme.adoc index 423f94ec0c9..c4a28621bff 100644 --- a/io.openems.edge.io.shelly/readme.adoc +++ b/io.openems.edge.io.shelly/readme.adoc @@ -9,5 +9,6 @@ Compatible with - https://www.shelly.com/de/products/shop/shelly-plus-plug-s-1[Shelly Plus Plug S] - https://www.shelly.com/de/products/shop/shelly-pro-3-em-120-a-1[Shelly Pro 3EM 3-Phase Meter] - https://www.shelly.com/de/products/shop/shelly-plus-1-pm[Shelly Plus 1PM] +- https://www.shelly.com/de/products/shop/shelly-pro-3-1[Shelly Pro 3] https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.io.shelly[Source Code icon:github[]] \ No newline at end of file diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/common/Utils.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/common/Utils.java index 14595f04ea6..ca4ac6ff5b4 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/common/Utils.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/common/Utils.java @@ -1,5 +1,6 @@ package io.openems.edge.io.shelly.common; +import io.openems.edge.common.channel.BooleanWriteChannel; import io.openems.edge.common.channel.Channel; import io.openems.edge.common.component.OpenemsComponent; @@ -26,4 +27,29 @@ public static String generateDebugLog(Channel relayChannel, Channel + *
  • Interface: Shelly3Pro + *
  • Type: Boolean + *
  • Range: On/Off + * + */ + DEBUG_RELAY_1(Doc.of(OpenemsType.BOOLEAN)), // + /** + * Relay Output 1. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: Boolean + *
    • Range: On/Off + *
    + */ + RELAY_1(new BooleanDoc() // + .accessMode(AccessMode.READ_WRITE) // + .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_1)), + /** + * Holds writes to Relay Output 2 for debugging. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: Boolean + *
    • Range: On/Off + *
    + */ + DEBUG_RELAY_2(Doc.of(OpenemsType.BOOLEAN)), // + /** + * Relay Output 2. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: Boolean + *
    • Range: On/Off + *
    + */ + RELAY_2(new BooleanDoc() // + .accessMode(AccessMode.READ_WRITE) // + .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_2)), + /** + * Holds writes to Relay Output 3 for debugging. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: Boolean + *
    • Range: On/Off + *
    + */ + DEBUG_RELAY_3(Doc.of(OpenemsType.BOOLEAN)), // + /** + * Relay Output 3. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: Boolean + *
    • Range: On/Off + *
    + */ + RELAY_3(new BooleanDoc() // + .accessMode(AccessMode.READ_WRITE) // + .onChannelSetNextWriteMirrorToDebugChannel(ChannelId.DEBUG_RELAY_3)), + /** + * Slave Communication Failed Fault. + * + *
      + *
    • Interface: Shelly3Pro + *
    • Type: State + *
    + */ + SLAVE_COMMUNICATION_FAILED(Doc.of(Level.FAULT)); // + + private final Doc doc; + + private ChannelId(Doc doc) { + this.doc = doc; + } + + @Override + public Doc doc() { + return this.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#RELAY_3}. + * + * @return the Channel + */ + public default BooleanWriteChannel getRelay3Channel() { + return this.channel(ChannelId.RELAY_3); + } + + /** + * Gets the Relay Output 3. See {@link ChannelId#RELAY_3}. + * + * @return the Channel {@link Value} + */ + public default Value getRelay3() { + return this.getRelay3Channel().value(); + } + + /** + * Internal method to set the 'nextValue' on {@link ChannelId#RELAY_3} Channel. + * + * @param value the next value + */ + public default void _setRelay3(Boolean value) { + this.getRelay3Channel().setNextValue(value); + } + + /** + * Sets the Relay Output 3. See {@link ChannelId#RELAY_3}. + * + * @param value the next write value + * @throws OpenemsNamedException on error + */ + public default void setRelay3(boolean value) throws OpenemsNamedException { + this.getRelay3Channel().setNextWriteValue(value); + } + + /** + * Gets the Channel for {@link ChannelId#SLAVE_COMMUNICATION_FAILED}. + * + * @return the Channel + */ + public default StateChannel getSlaveCommunicationFailedChannel() { + return this.channel(ChannelId.SLAVE_COMMUNICATION_FAILED); + } + + /** + * Gets the Slave Communication Failed State. See + * {@link ChannelId#SLAVE_COMMUNICATION_FAILED}. + * + * @return the Channel {@link Value} + */ + public default Value getSlaveCommunicationFailed() { + return this.getSlaveCommunicationFailedChannel().value(); + } + + /** + * Internal method to set the 'nextValue' on + * {@link ChannelId#SLAVE_COMMUNICATION_FAILED} Channel. + * + * @param value the next value + */ + public default void _setSlaveCommunicationFailed(boolean value) { + this.getSlaveCommunicationFailedChannel().setNextValue(value); + } +} diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3/IoShellyPro3Impl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3/IoShellyPro3Impl.java new file mode 100644 index 00000000000..888ceb58da3 --- /dev/null +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3/IoShellyPro3Impl.java @@ -0,0 +1,167 @@ +package io.openems.edge.io.shelly.shellypro3; + +import static io.openems.common.utils.JsonUtils.getAsBoolean; +import static io.openems.common.utils.JsonUtils.getAsJsonObject; +import static io.openems.edge.io.shelly.common.Utils.generateDebugLog; + +import java.util.Objects; + +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +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; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; + +import io.openems.edge.bridge.http.api.BridgeHttp; +import io.openems.edge.bridge.http.api.BridgeHttpFactory; +import io.openems.edge.bridge.http.api.HttpError; +import io.openems.edge.bridge.http.api.HttpResponse; +import io.openems.edge.common.channel.BooleanWriteChannel; +import io.openems.edge.common.component.AbstractOpenemsComponent; +import io.openems.edge.common.component.OpenemsComponent; +import io.openems.edge.common.event.EdgeEventConstants; +import io.openems.edge.io.api.DigitalOutput; + +@Designate(ocd = Config.class, factory = true) +@Component(// + name = "IO.Shelly.Pro3", // + immediate = true, // + configurationPolicy = ConfigurationPolicy.REQUIRE // +) +@EventTopics({ // + EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // +}) + +public class IoShellyPro3Impl extends AbstractOpenemsComponent + implements IoShellyPro3, DigitalOutput, OpenemsComponent, EventHandler { + + private final Logger log = LoggerFactory.getLogger(IoShellyPro3Impl.class); + private final BooleanWriteChannel[] digitalOutputChannels; + + private String baseUrl; + + @Reference + private BridgeHttpFactory httpBridgeFactory; + private BridgeHttp httpBridge; + + public IoShellyPro3Impl() { + super(// + OpenemsComponent.ChannelId.values(), // + DigitalOutput.ChannelId.values(), // + IoShellyPro3.ChannelId.values() // + ); + this.digitalOutputChannels = new BooleanWriteChannel[] { // + this.channel(IoShellyPro3.ChannelId.RELAY_1), // + this.channel(IoShellyPro3.ChannelId.RELAY_2), // + this.channel(IoShellyPro3.ChannelId.RELAY_3), // + }; + } + + @Activate + protected void activate(ComponentContext context, Config config) { + super.activate(context, config.id(), config.alias(), config.enabled()); + this.baseUrl = "http://" + config.ip(); + this.httpBridge = this.httpBridgeFactory.get(); + + for (int i = 0; i < 3; i++) { + final int relayIndex = i; + String url = this.baseUrl + "/rpc/Switch.GetStatus?id=" + relayIndex; + this.httpBridge.subscribeJsonEveryCycle(url, (result, error) -> { + this.processHttpResult(result, error, relayIndex); + }); + } + } + + @Deactivate + protected void deactivate() { + this.httpBridgeFactory.unget(this.httpBridge); + this.httpBridge = null; + super.deactivate(); + } + + @Override + public BooleanWriteChannel[] digitalOutputChannels() { + return this.digitalOutputChannels; + } + + @Override + public String debugLog() { + return generateDebugLog(this.digitalOutputChannels); + } + + @Override + public void handleEvent(Event event) { + if (!this.isEnabled()) { + return; + } + + switch (event.getTopic()) { + case EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE // + -> this.eventExecuteWrite(); + } + } + + // NOTE: this method is called once per each relay + private void processHttpResult(HttpResponse result, HttpError error, int relayIndex) { + this._setSlaveCommunicationFailed(result == null); + + Boolean isOn = null; + + if (error != null) { + this.logDebug(this.log, error.getMessage()); + + } else { + try { + var switchStatus = getAsJsonObject(result.data()); + isOn = getAsBoolean(switchStatus, "output"); + + } catch (Exception e) { + this.logError(this.log, "Error processing HTTP response: " + e.getMessage()); + return; + } + } + + switch (relayIndex) { + case 0 -> this._setRelay1(isOn); + case 1 -> this._setRelay2(isOn); + case 2 -> this._setRelay3(isOn); + } + } + + /** + * Execute on Cycle Event "Execute Write". + */ + private void eventExecuteWrite() { + for (int i = 0; i < this.digitalOutputChannels.length; i++) { + this.executeWrite(this.digitalOutputChannels[i], i); + } + } + + private void executeWrite(BooleanWriteChannel channel, int index) { + var readValue = channel.value().get(); + var writeValue = channel.getNextWriteValueAndReset(); + if (writeValue.isEmpty()) { + return; + } + if (Objects.equals(readValue, writeValue.get())) { + return; + } + final String url = this.baseUrl + "/relay/" + index + "?turn=" + (writeValue.get() ? "on" : "off"); + this.httpBridge.get(url).whenComplete((t, e) -> { + if (e != null) { + this.logError(this.log, "HTTP request failed: " + e.getMessage()); + this._setSlaveCommunicationFailed(true); + } + }); + } + +} diff --git a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java index dfa0c2085b4..15db1dac66f 100644 --- a/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java +++ b/io.openems.edge.io.shelly/src/io/openems/edge/io/shelly/shellypro3em/IoShellyPro3EmImpl.java @@ -40,7 +40,7 @@ @Designate(ocd = Config.class, factory = true) @Component(// - name = "IO.Shelly.Pro.3EM", // + name = "IO.Shelly.Pro3EM", // immediate = true, // configurationPolicy = ConfigurationPolicy.REQUIRE // ) diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java new file mode 100644 index 00000000000..ed4f8d960b6 --- /dev/null +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/IoShellyPro3ImplTest.java @@ -0,0 +1,23 @@ +package io.openems.edge.io.shelly.shellypro3; + +import org.junit.Test; + +import io.openems.edge.bridge.http.dummy.DummyBridgeHttpFactory; +import io.openems.edge.common.test.ComponentTest; + +public class IoShellyPro3ImplTest { + + private static final String COMPONENT_ID = "io0"; + + @Test + public void test() throws Exception { + new ComponentTest(new IoShellyPro3Impl()) // + .addReference("httpBridgeFactory", DummyBridgeHttpFactory.ofDummyBridge()) // + .activate(MyConfig.create() // + .setId(COMPONENT_ID) // + .setIp("127.0.0.1") // + .build()) // + ; + } + +} \ No newline at end of file diff --git a/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/MyConfig.java b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/MyConfig.java new file mode 100644 index 00000000000..3d58882f769 --- /dev/null +++ b/io.openems.edge.io.shelly/test/io/openems/edge/io/shelly/shellypro3/MyConfig.java @@ -0,0 +1,50 @@ +package io.openems.edge.io.shelly.shellypro3; + +import io.openems.common.test.AbstractComponentConfig; + +@SuppressWarnings("all") +public class MyConfig extends AbstractComponentConfig implements Config { + + protected static class Builder { + private String id; + private String ip; + + private Builder() { + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setIp(String ip) { + this.ip = ip; + return this; + } + + public MyConfig build() { + return new MyConfig(this); + } + } + + /** + * Create a Config builder. + * + * @return a {@link Builder} + */ + public static Builder create() { + return new Builder(); + } + + private final Builder builder; + + private MyConfig(Builder builder) { + super(Config.class, builder.id); + this.builder = builder; + } + + @Override + public String ip() { + return this.builder.ip; + } +} \ No newline at end of file