diff --git a/CODEOWNERS b/CODEOWNERS index 41e5ecba69f28..58703359ae9ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -251,6 +251,7 @@ /bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.solaredge/ @alexf2015 /bundles/org.openhab.binding.solarlog/ @johannrichard +/bundles/org.openhab.binding.solarmax/ @jamietownsend /bundles/org.openhab.binding.somfymylink/ @loungeflyz /bundles/org.openhab.binding.somfytahoma/ @octa22 /bundles/org.openhab.binding.sonos/ @kgoderis @lolodomo diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 1b0f7a7afa26e..3390302afe325 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1236,6 +1236,11 @@ org.openhab.binding.solarlog ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.solarmax + ${project.version} + org.openhab.addons.bundles org.openhab.binding.somfymylink diff --git a/bundles/org.openhab.binding.solarmax/NOTICE b/bundles/org.openhab.binding.solarmax/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.solarmax/README.md b/bundles/org.openhab.binding.solarmax/README.md new file mode 100644 index 0000000000000..31dab23c8d4a0 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/README.md @@ -0,0 +1,70 @@ +# SolarMax Binding + +This binding supports SolarMax PV inverters. + +## Supported Things + +The SolarMax MT Series is support. (tested with 8MT2 devices) + +## Discovery + +Auto-discovery is currently not available. + +## Thing Configuration + +The IP address and port number (default 12345) of the device needs to be configured. + + +``` +############################## openHAB SolarMax Binding ############################# + +# The IP address or hostname of the SolarMax device +#host=192.168.1.151|SolarMax1 + +# The port number configured on the SolarMax device +# Default is 12345 +#portNumber=12345 + +# The refresh interval (in seconds) +# Default is 15 +#refreshInterval=15 +``` + +## Channels + +| channel | type | description | +| ------------------------ | ------ | ------------------------------------------- | +| LastUpdated | Point | When was the data last read from the device | +| SoftwareVersion | Point | Software Version installed on the SolarMax device | +| BuildNumber | Point | Firmware Build Number installed on the SolarMax device | +| Startups | Point | Number of times the device has started | +| AcPhase1Current | Point | Ac Phase 1 Current in Amps | +| AcPhase2Current | Point | Ac Phase 2 Current in Amps | +| AcPhase3Current | Point | Ac Phase 3 Current in Amps | +| EnergyGeneratedToday | Point | Energy Generated Today in wH | +| EnergyGeneratedTotal | Point | Energy Generated since recording began in wH | +| OperatingHours | Point | Operating Hours since recording began in H | +| EnergyGeneratedYesterday | Point | Energy Generated Yesterday in wH | +| EnergyGeneratedLastMonth | Point | Energy Generated Last Month in wH | +| EnergyGeneratedLastYear | Point | Energy Generated Last Year in wH | +| EnergyGeneratedThisMonth | Point | Energy Generated This Month in wH | +| EnergyGeneratedThisYear | Point | Energy Generated This Year in wH | +| Current Power Generated | Point | Power currently being generated in w | +| AcFrequency | Point | AcFrequency in Hz | +| AcPhase1Voltage | Point | Ac Phase1 Voltage in V | +| AcPhase2Voltage | Point | Ac Phase2 Voltage in V | +| AcPhase3Voltage | Point | Ac Phase3 Voltage in V | +| HeatSinkTemperature | Point | Heat Sink Temperature in degrees celcius | + +## Full Example + +Example Thing Configuration +``` +UID: solarmax:inverter:7a56fa7252 +label: SolarMax Power Inverter East +thingTypeUID: solarmax:inverter +configuration: + host: 192.168.1.151 + refreshInterval: 15 + portNumber: 12345 +``` diff --git a/bundles/org.openhab.binding.solarmax/cfg/solarmax.cfg b/bundles/org.openhab.binding.solarmax/cfg/solarmax.cfg new file mode 100644 index 0000000000000..dcbc0fffdff15 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/cfg/solarmax.cfg @@ -0,0 +1,12 @@ +############################## openHAB SolarMax Binding ############################# + +# The IP address or hostname of the SolarMax device +#host=192.168.1.151|SolarMax1 + +# The port number configured on the SolarMax device +# Default is 12345 +#portNumber=12345 + +# The refresh interval (in seconds) +# Default is 15 +#refreshInterval=15 diff --git a/bundles/org.openhab.binding.solarmax/pom.xml b/bundles/org.openhab.binding.solarmax/pom.xml new file mode 100644 index 0000000000000..32d07a6947dfa --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.solarmax + + openHAB Add-ons :: Bundles :: SolarMax Binding + + diff --git a/bundles/org.openhab.binding.solarmax/src/main/feature/feature.xml b/bundles/org.openhab.binding.solarmax/src/main/feature/feature.xml new file mode 100644 index 0000000000000..bc7ddfe8374b6 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.solarmax/${project.version} + + diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxBindingConstants.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxBindingConstants.java new file mode 100644 index 0000000000000..8531d48a41b2e --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxBindingConstants.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link SolarMaxBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jamie Townsend - Initial contribution + */ +@NonNullByDefault +public class SolarMaxBindingConstants { + + private static final String BINDING_ID = "solarmax"; + private static final String THING_TYPE_ID = "inverter"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SOLARMAX = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + + // Config - do we really need to define all the values from SolarMaxConfiguration here?? I think not + // ... + + // Channels - do we really need to define all the values from SolarMaxChannel here?? I think not + // ... +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxChannel.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxChannel.java new file mode 100644 index 0000000000000..126650d4fb5c3 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxChannel.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal; + +import org.openhab.binding.solarmax.internal.connector.SolarMaxCommandKey; + +/** + * The {@link SolarMaxChannel} Enum defines common constants, which are + * used across the whole binding. + * + * @author Jamie Townsend - Initial contribution + */ +public enum SolarMaxChannel { + + // CHANNEL_UPDATE_VALUES_FROM_DEVICE("UpdateValuesFromDevice"), + CHANNEL_LAST_UPDATED("LastUpdated"), // + // CHANNEL_DEVICE_ADDRESS(SolarMaxCommandKey.DeviceAddress.name()), + CHANNEL_SOFTWARE_VERSION(SolarMaxCommandKey.SoftwareVersion.name()), + CHANNEL_BUILD_NUMBER(SolarMaxCommandKey.BuildNumber.name()), + CHANNEL_STARTUPS(SolarMaxCommandKey.Startups.name()), + CHANNEL_AC_PHASE1_CURRENT(SolarMaxCommandKey.AcPhase1Current.name()), + CHANNEL_AC_PHASE2_CURRENT(SolarMaxCommandKey.AcPhase2Current.name()), + CHANNEL_AC_PHASE3_CURRENT(SolarMaxCommandKey.AcPhase3Current.name()), + CHANNEL_ENERGY_GENERATED_TODAY(SolarMaxCommandKey.EnergyGeneratedToday.name()), + CHANNEL_ENERGY_GENERATED_TOTAL(SolarMaxCommandKey.EnergyGeneratedTotal.name()), + CHANNEL_OPERATING_HOURS(SolarMaxCommandKey.OperatingHours.name()), + CHANNEL_ENERGY_GENERATED_YESTERDAY(SolarMaxCommandKey.EnergyGeneratedYesterday.name()), + CHANNEL_ENERGY_GENERATED_LAST_MONTH(SolarMaxCommandKey.EnergyGeneratedLastMonth.name()), + CHANNEL_ENERGY_GENERATED_LAST_YEAR(SolarMaxCommandKey.EnergyGeneratedLastYear.name()), + CHANNEL_ENERGY_GENERATED_THIS_MONTH(SolarMaxCommandKey.EnergyGeneratedThisMonth.name()), + CHANNEL_ENERGY_GENERATED_THIS_YEAR(SolarMaxCommandKey.EnergyGeneratedThisYear.name()), + CHANNEL_CURRENT_POWER_GENERATED(SolarMaxCommandKey.CurrentPowerGenerated.name()), + CHANNEL_AC_FREQUENCY(SolarMaxCommandKey.AcFrequency.name()), + CHANNEL_AC_PHASE1_VOLTAGE(SolarMaxCommandKey.AcPhase1Voltage.name()), + CHANNEL_AC_PHASE2_VOLTAGE(SolarMaxCommandKey.AcPhase2Voltage.name()), + CHANNEL_AC_PHASE3_VOLTAGE(SolarMaxCommandKey.AcPhase3Voltage.name()), + CHANNEL_HEAT_SINK_TEMPERATUR(SolarMaxCommandKey.HeatSinkTemperature.name()) + + ; + + private final String channelId; + + private SolarMaxChannel(String channelId) { + this.channelId = channelId; + } + + public String getChannelId() { + return channelId; + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxConfiguration.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxConfiguration.java new file mode 100644 index 0000000000000..8482b9b5587e7 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SolarMaxConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jamie Townsend - Initial contribution + */ +@NonNullByDefault +public class SolarMaxConfiguration { + public String host = ""; // this will always need to be overridden + public int portNumber = 12345; // default value is 12345 + + public int refreshInterval = 15; // default value is 15 +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandler.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandler.java new file mode 100644 index 0000000000000..620c8436006a3 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandler.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.solarmax.internal.connector.SolarMaxCommandKey; +import org.openhab.binding.solarmax.internal.connector.SolarMaxConnector; +import org.openhab.binding.solarmax.internal.connector.SolarMaxData; +import org.openhab.binding.solarmax.internal.connector.SolarMaxException; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarMaxHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jamie Townsend - Initial contribution + */ +public class SolarMaxHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(SolarMaxHandler.class); + + @Nullable + private SolarMaxConfiguration config; + + @Nullable + private ScheduledFuture pollingJob; + + public SolarMaxHandler(final Thing thing) { + super(thing); + } + + @Override + public void handleCommand(final ChannelUID channelUID, final Command command) { + // Read only + + // logger.debug("handleCommand channel: {} command: {}", channelUID, command); + // String ch = channelUID.getId(); + // // if (command instanceof RefreshType) { + // // updateChannels(); + // // return; + // // } + // if (ch.equals(SolarMaxChannel.CHANNEL_UPDATE_VALUES_FROM_DEVICE.name())) { + // // if (ch.equals(SolarMaxBindingConstants.CHANNEL_UPDATE_VALUES_FROM_DEVICE)) { + // updateValuesFromDevice(); + // } + } + + @Override + public void initialize() { + logger.debug("Initializing SolarMax"); + + config = getConfigAs(SolarMaxConfiguration.class); + + configurePolling(); // Setup the scheduler + } + + /** + * This is called to start the refresh job and also to reset that refresh job when a config change is done. + */ + private void configurePolling() { + logger.debug("Polling data from {} at {}:{} every {} seconds ", getThing().getUID(), this.config.host, + this.config.portNumber, this.config.refreshInterval); + if (this.config.refreshInterval > 0) { + if (pollingJob == null || pollingJob.isCancelled()) { + pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, this.config.refreshInterval, + TimeUnit.SECONDS); + } + } + } + + @Override + public void dispose() { + logger.debug("Disposing SolarMax Handler Thing"); + // isRunning = false; + if (pollingJob != null && !pollingJob.isCancelled()) { + pollingJob.cancel(true); + } + pollingJob = null; + } + + /** + * Polling event used to get data from the SolarMax device + */ + private Runnable pollingRunnable = () -> { + updateValuesFromDevice(); + }; + + private synchronized void updateValuesFromDevice() { + + logger.debug("Updating data from {} at {}:{} ", getThing().getUID(), this.config.host, this.config.portNumber); + // get the data from the SolarMax device + try { + SolarMaxData solarMaxData = SolarMaxConnector.getAllValuesFromSolarMax(config.host, config.portNumber); + + if (solarMaxData.wasCommunicationSuccessful()) { + updateChannels(solarMaxData); + updateStatus(ThingStatus.ONLINE); + return; + } + } catch (SolarMaxException e) { + logger.debug("Error refreshing source {} at {}:{} - {}", getThing().getUID(), this.config.host, + this.config.portNumber, e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Communication error with the device. Please retry later."); + } + } + + /* + * Update the channels + */ + private void updateChannels(SolarMaxData solarMaxData) { + logger.debug("Updating all channels"); + for (SolarMaxChannel channelConfig : SolarMaxChannel.values()) { + String channelId = channelConfig.getChannelId(); + + Channel channel = getThing().getChannel(channelId); + + // there are two special channels, where the values don't come from the device + // if (channelId.equals(SolarMaxChannel.CHANNEL_UPDATE_VALUES_FROM_DEVICE.getChannelId())) { + // // channel isn't read from the device, so ignore this + + // } else + if (channelId.equals(SolarMaxChannel.CHANNEL_LAST_UPDATED.getChannelId())) { + // channel shows when the device was last read, so handle it specially + State state = solarMaxData.getDataDateTime(); + logger.debug("Update channel state: {} - {}", channelId, state); + updateState(channel.getUID(), state); + + } else + // must be somthing to collect from the device, so... + if (solarMaxData.has(SolarMaxCommandKey.valueOf(channelId))) { + + if (channel == null) { + logger.error("No channel found with id: {}", channelId); + } + // State state = getState(value, channel); + // return new DecimalType(versionAsInt); + State state = solarMaxData.get(SolarMaxCommandKey.valueOf(channelId)); + + if (channel != null && state != null) { + logger.debug("Update channel state: {} - {}", channelId, state); + updateState(channel.getUID(), state); + } else { + logger.debug("Error refreshing channel {}: {}", getThing().getUID(), channelId); + + } + } + + } + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandlerFactory.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandlerFactory.java new file mode 100644 index 0000000000000..fc5b8a1487afd --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandlerFactory.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal; + +import static org.openhab.binding.solarmax.internal.SolarMaxBindingConstants.THING_TYPE_SOLARMAX; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarMaxHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jamie Townsend - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.solarmax", service = ThingHandlerFactory.class) +public class SolarMaxHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(SolarMaxHandlerFactory.class); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SOLARMAX); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_SOLARMAX.equals(thingTypeUID)) { + logger.debug("Creating Handler {}", thing.getUID()); + return new SolarMaxHandler(thing); + } else { + logger.warn("Creating Handler failed - unsupported Thing Type {}", thingTypeUID); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxCommandKey.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxCommandKey.java new file mode 100644 index 0000000000000..c21bad3918db2 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxCommandKey.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +/** + * The {@link SolarMaxCommandKey} enum defines the commands that are understood by the SolarMax device + * + * @author Jamie Townsend - Initial contribution + */ +public enum SolarMaxCommandKey { + + // Valid commands which returned a non-null value during testing + // DeviceAddress("ADR"), // device number - only used if the devices are linked serially + // UNKNOWN_AMM("AMM"), // + BuildNumber("BDN"), // + Startups("CAC"), // + // UNKNOWN_CID("CID"), // + // UNKNOWN_CPG("CPG"), // + // UNKNOWN_CPL("CPL"), // + // UNKNOWN_CP1("CP1"), // + // UNKNOWN_CP2("CP2"), // + // UNKNOWN_CP3("CP3"), // + // UNKNOWN_CP4("CP4"), // + // UNKNOWN_CP5("CP5"), // + // UNKNOWN_CYC("CYC"), // + // UNKNOWN_DIN("DIN"), // + // UNKNOWN_DMO("DMO"), // + // UNKNOWN_ETH("ETH"), // + // UNKNOWN_FH2("FH2"), // + // UNKNOWN_FQR("FQR"), // + // UNKNOWN_FWV("FWV"), // + // UNKNOWN_IAA("IAA"), // + // UNKNOWN_IED("IED"), // + // UNKNOWN_IEE("IEE"), // + // UNKNOWN_IEM("IEM"), // + // UNKNOWN_ILM("ILM"), // + AcPhase1Current("IL1"), // + AcPhase2Current("IL2"), // + AcPhase3Current("IL3"), // + // UNKNOWN_IP4("IP4"), // + // UNKNOWN_ISL("ISL"), // + // UNKNOWN_ITS("ITS"), // + EnergyGeneratedToday("KDY"), // + // UNKNOWN_KFS("KFS"), // + OperatingHours("KHR"), // + // UNKNOWN_KHS("KHS"), // + EnergyGeneratedYesterday("KLD"), // + EnergyGeneratedLastMonth("KLM"), // + EnergyGeneratedLastYear("KLY"), // + EnergyGeneratedThisMonth("KMT"), // + // UNKNOWN_KTS("KTS"), // + EnergyGeneratedTotal("KT0"), // + EnergyGeneratedThisYear("KYR"), // + // Language("LAN"), // + // MacAddress("MAC"), // + CurrentPowerGenerated("PAC"), // + // UNKNOWN_PAE("PAE"), // + // UNKNOWN_PAM("PAM"), // + // UNKNOWN_PDA("PDA"), // + // UNKNOWN_PDC("PDC"), // + // UNKNOWN_PFA("PFA"), // + // PowerInstalled("PIN"), // + // UNKNOWN_PLR("PLR"), // + // UNKNOWN_PPC("PPC"), // + // AcPowerPercent("PRL"), // + // UNKNOWN_PSF("PSF"), // + // UNKNOWN_PSR("PSR"), // + // UNKNOWN_PSS("PSS"), // + // UNKNOWN_QAC("QAC"), // + // UNKNOWN_QMO("QMO"), // + // UNKNOWN_QUC("QUC"), // + // UNKNOWN_RA1("RA1"), // + // UNKNOWN_RA2("RA2"), // + // UNKNOWN_RB1("RB1"), // + // UNKNOWN_RB2("RB2"), // + // UNKNOWN_REL("REL"), // + // UNKNOWN_RH1("RH1"), // + // UNKNOWN_RH2("RH2"), // + // UNKNOWN_RPR("RPR"), // + // UNKNOWN_RSD("RSD"), // + // UNKNOWN_SAC("SAC"), // + // UNKNOWN_SAL("SAL"), // + // UNKNOWN_SAM("SAM"), // + // UNKNOWN_SCH("SCH"), // + // UNKNOWN_SNM("SNM"), // IP Broadcast Address?? + // UNKNOWN_SPS("SPS"), // + // UNKNOWN_SRD("SRD"), // + // UNKNOWN_SRS("SRS"), // + SoftwareVersion("SWV"), // + // OperatingState("SYS"), // + // UNKNOWN_TCP("TCP"), // probably port number (12345) + // UNKNOWN_TI1("TI1"), // + HeatSinkTemperature("TKK"), // + // UNKNOWN_TL1("TL1"), // + // UNKNOWN_TL3("TL3"), // + // UNKNOWN_TND("TND"), // + AcFrequency("TNF"), // + // UNKNOWN_TNH("TNH"), // + // UNKNOWN_TNL("TNL"), // + // UNKNOWN_TP1("TP1"), // + // UNKNOWN_TP2("TP2"), // + // UNKNOWN_TP3("TP3"), // + // UNKNOWN_TV0("TV0"), // + // UNKNOWN_TV1("TV1"), // + // Type("TYP"), // + // UNKNOWN_UA2("UA2"), // + // UNKNOWN_UB2("UB2"), // + // UNKNOWN_UGD("UGD"), // + // UNKNOWN_UI1("UI1"), // + // UNKNOWN_UI2("UI2"), // + // UNKNOWN_UI3("UI3"), // + // UNKNOWN_ULH("ULH"), // + // UNKNOWN_ULL("ULL"), // + AcPhase1Voltage("UL1"), // + AcPhase2Voltage("UL2"), // + AcPhase3Voltage("UL3"), // + // UNKNOWN_UMX("UMX"), // + // UNKNOWN_UM1("UM1"), // + // UNKNOWN_UM2("UM2"), // + // UNKNOWN_UM3("UM3"), // + // UNKNOWN_UPD("UPD"), // + // UNKNOWN_UZK("UZK"), // + // UNKNOWN_VCM("VCM"), // + UNKNOWN("UNKNOWN") // really unknown - shouldn't ever be sent to the device + ; + + // Valid commands which returned a null/empty value during testing + // FFK, FRT, GCP, ITN, PLD, PLE, PLF, PLS, PPO, TV2, VLE, VLI, VLO + + private String commandKey; + + private SolarMaxCommandKey(String commandKey) { + this.commandKey = commandKey; + } + + public String getCommandKey() { + return this.commandKey; + } + + public static SolarMaxCommandKey getKeyFromString(String commandKey) { + + for (SolarMaxCommandKey key : SolarMaxCommandKey.values()) { + if (key.commandKey.equals(commandKey)) { + return key; + } + } + return UNKNOWN; + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnectionException.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnectionException.java new file mode 100644 index 0000000000000..b57e78d1ce374 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnectionException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SolarMaxException} Exception is used for connection problems trying to communicate with the SolarMax + * device. + * + * @author Jamie Townsend - Initial contribution + */ +@NonNullByDefault +public class SolarMaxConnectionException extends SolarMaxException { + + private static final long serialVersionUID = 1L; + + public SolarMaxConnectionException(final String message, final Throwable cause) { + super(message, cause); + } + + public SolarMaxConnectionException(final Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnector.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnector.java new file mode 100644 index 0000000000000..a9a1daddee59b --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxConnector.java @@ -0,0 +1,409 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * The {@link SolarMaxConnector} class is used to communicated with the SolarMax device (on a binary level) + * + * With a little help from https://github.com/sushiguru/solar-pv/blob/master/solmax/pv.php + * + * @author Jamie Townsend - Initial contribution + */ +public class SolarMaxConnector { + + /** + * default port number of SolarMax devices is... + */ + final private static int DEFAULT_PORT = 12345; + + private static final Logger logger = LoggerFactory.getLogger(SolarMaxConnector.class); + + /** + * default timeout for socket connections is 1 second + */ + private static int connectionTimeout = 1000; + + /** + * default timeout for socket responses is 10 seconds + */ + private static int responseTimeout = 10000; + + /** + * gets all known values from the SolarMax device addressable at host:port + * + * @param host hostname or ip address of the SolarMax device to be contacted + * @param port port the SolarMax is listening on (default is 12345) + * @param commandList a list of commands to be sent to the SolarMax device + * @return + * @throws UnknownHostException if the host is unknown + * @throws SolarMaxException if some other exception occurs + */ + public static SolarMaxData getAllValuesFromSolarMax(final String host, int port) throws SolarMaxException { + + List commandList = new ArrayList<>(); + + for (SolarMaxCommandKey solarMaxCommandKey : SolarMaxCommandKey.values()) { + if (solarMaxCommandKey != SolarMaxCommandKey.UNKNOWN) { + commandList.add(solarMaxCommandKey); + } + } + + SolarMaxData solarMaxData = new SolarMaxData(); + + // get the data from the SolarMax device. If we didn't get as many values back as we asked for, there were + // communications problems, so set communicationSuccessful appropriately + + Map valuesFromSolarMax = getValuesFromSolarMax(host, port, commandList); + boolean allCommandsAnswered = true; + for (SolarMaxCommandKey solarMaxCommandKey : commandList) { + if (!valuesFromSolarMax.containsKey(solarMaxCommandKey)) { + allCommandsAnswered = false; + break; + } + } + solarMaxData.setDataDateTime(ZonedDateTime.now()); + solarMaxData.setCommunicationSuccessful(allCommandsAnswered); + solarMaxData.setData(valuesFromSolarMax); + + return solarMaxData; + } + + /** + * gets values from the SolarMax device addressable at host:port + * + * @param host hostname or ip address of the SolarMax device to be contacted + * @param port port the SolarMax is listening on (default is 12345) + * @param commandList a list of commands to be sent to the SolarMax device + * @return + * @throws UnknownHostException if the host is unknown + * @throws SolarMaxException if some other exception occurs + */ + private static Map getValuesFromSolarMax(final String host, int port, + final List commandList) throws SolarMaxException { + + Socket socket; + + Map returnMap = new HashMap<>(); + + // SolarMax can't answer correclty if too many commands are send in a single request, so limit it to 16 at a + // time + int maxConcurrentCommands = 16; + int requestsRequired = (commandList.size() / maxConcurrentCommands); + if (commandList.size() % maxConcurrentCommands != 0) { + requestsRequired = requestsRequired + 1; + } + for (int requestNumber = 0; requestNumber < requestsRequired; requestNumber++) { + logger.debug(" Requesting data from {}:{} with timeout of {}ms", host, port, responseTimeout); + + int firstCommandNumber = requestNumber * maxConcurrentCommands; + int lastCommandNumber = (requestNumber + 1) * maxConcurrentCommands; + if (lastCommandNumber > commandList.size()) { + lastCommandNumber = commandList.size(); + } + List commandsToSend = commandList.subList(firstCommandNumber, lastCommandNumber); + + try { + socket = getSocketConnection(host, port); + } catch (UnknownHostException e) { + throw new SolarMaxConnectionException(e); + } + returnMap.putAll(getValuesFromSolarMax(socket, commandsToSend)); + + // SolarMax can't deal with requests too close to one another, so just wait a moment + try { + Thread.sleep(10); + } catch (InterruptedException e) { + // do nothing + } + } + return returnMap; + } + + private static String getCommandString(List commandList) { + String commandString = ""; + for (SolarMaxCommandKey command : commandList) { + if (commandString != "") { + commandString = commandString + ";"; + } + commandString = commandString + command.getCommandKey(); + } + return commandString; + } + + private static Map getValuesFromSolarMax(final Socket socket, + final List commandList) throws SolarMaxException { + OutputStream outputStream = null; + InputStream inputStream = null; + try { + outputStream = socket.getOutputStream(); + inputStream = socket.getInputStream(); + + return getValuesFromSolarMax(outputStream, inputStream, commandList); + + } catch (final SolarMaxException | IOException e) { + throw new SolarMaxException("Error getting input/output streams from socket", e); + } finally { + try { + socket.close(); + if (outputStream != null) { + outputStream.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } catch (final IOException e) { + // ignore the error, we're dying anyway... + } + } + } + + private static Map getValuesFromSolarMax(final OutputStream outputStream, + final InputStream inputStream, final List commandList) throws SolarMaxException { + + Map returnedValues; + String commandString = getCommandString(commandList); + String request = contructRequest(commandString); + try { + + // hard code it for now + // request = "{FB;01;46|64:KDY;KMT;KYR;KT0;TNF;TKK;PAC;PRL;IL1;IDC;UL1;UDC;SYS|1199}"; + // send the message out + logger.trace(" ==>: {}", request); + + outputStream.write(request.getBytes()); + // outputStream.flush(); + + String response = ""; + byte[] responseByte = new byte[1]; + + // get everything from the stream + while (true) { + // read one byte from the stream + int bytesRead = inputStream.read(responseByte); + + // if there was nothing left, break + if (bytesRead < 1) { + break; + } + + // add the received byte to the response + final String responseString = new String(responseByte); + response = response + responseString; + + // if it was the final expected character "}", break + if ("}".equals(responseString)) { + break; + } + } + + logger.trace(" <==: {}", response); + + if (!validateResponse(response)) { + throw new SolarMaxException("Invalid response received: " + response); + } + + returnedValues = extractValuesFromResponse(response); + + return returnedValues; + + } catch (IOException e) { + logger.debug("Error communicating via input/output streams: {} ", e.getMessage()); + throw new SolarMaxException(e); + } + } + + /** + * @param response e.g. + * "{01;FB;6D|64:KDY=82;KMT=8F;KYR=23F7;KT0=72F1;TNF=1386;TKK=28;PAC=1F70;PRL=28;IL1=236;UL1=8F9;SYS=4E28,0|19E5}" + * @return a map of keys and values + */ + static Map extractValuesFromResponse(String response) { + + final Map responseMap = new HashMap<>(); + + // in case there is no response + if (response.indexOf("|") == -1) { + logger.warn("Response doesn't contain data. Response: {}", response); + return responseMap; + } + + // extract the body first + // start by getting the part of the response between the two pipes + String body = response.substring(response.indexOf("|") + 1, response.lastIndexOf("|")); + + // the name/value pairs now lie after the ":" + body = body.substring(body.indexOf(":") + 1); + + // split into an array of name=value pairs + String[] entries = body.split(";"); + for (String entry : entries) { + + if (entry.length() != 0) { + // could be split on "=" instead of fixed length or made to respect length of command, but they're all 3 + // characters long (then plus "=") + String str = entry.substring(0, 3); + + String responseValue = (entry.length() >= 5) ? entry.substring(4) : null; + + SolarMaxCommandKey key = SolarMaxCommandKey.getKeyFromString(str); + if (key != SolarMaxCommandKey.UNKNOWN) { + responseMap.put(key, responseValue); + } + } + } + + return responseMap; + } + + private static Socket getSocketConnection(final String host, int port) + throws SolarMaxConnectionException, UnknownHostException { + + port = (port == 0) ? DEFAULT_PORT : port; + + Socket socket; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress(host, port), connectionTimeout); + socket.setSoTimeout(responseTimeout); + } catch (final UnknownHostException e) { + throw e; + } catch (final IOException e) { + throw new SolarMaxConnectionException("Error connecting to port '" + port + "' on host '" + host + "'", e); + } + + return socket; + } + + public static boolean connectionTest(final String host, int port) throws UnknownHostException { + + Socket socket = null; + + try { + socket = getSocketConnection(host, port); + } catch (SolarMaxConnectionException e) { + return false; + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + // ignore any error while trying to close the socket + } + } + } + + return true; + } + + /** + * @return timeout for connections in milliseconds + */ + public static int getConnectionTimeout() { + return connectionTimeout; + } + + /** + * @param connectionTimeout timeout for connections in milliseconds + */ + public static void setConnectionTimeout(int connectionTimeout) { + SolarMaxConnector.connectionTimeout = connectionTimeout; + } + + /** + * @return timeout for responses in milliseconds + */ + public static int getResponseTimeout() { + return responseTimeout; + } + + /** + * @param responseTimeout timeout for responses in milliseconds + */ + public static void setResponseTimeout(int responseTimeout) { + SolarMaxConnector.responseTimeout = responseTimeout; + } + + /** + * @param destinationDevice device number - used if devices are daisy-chained. Normally it will be "1" + * @param questions appears to be able to handle multiple commands. For now, one at a time is good fishing + * @return the request to be sent to the SolarMax device + */ + static String contructRequest(final String questions) { + String src = "FB"; + String dstHex = String.format("%02X", 1); // destinationDevice defaults to 1 and is ignored with TCP/IP + String len = "00"; + String cs = "0000"; + // String msg = is_array(questions) ? "64:" + implode(';', questions) : "64:" + questions; + String msg = "64:" + questions; + int lenInt = ("{" + src + ";" + dstHex + ";" + len + "|" + msg + "|" + cs + "}").length(); + + // given the following, I'd expect problems if the request is longer than 255 characters. Since I'm not sure + // though, I won't fixe what isn't (yet) broken + String lenHex = String.format("%02X", lenInt); + + String checksum = calculateChecksum16(src + ";" + dstHex + ";" + lenHex + "|" + msg + "|"); + + return "{" + src + ";" + dstHex + ";" + lenHex + "|" + msg + "|" + checksum + "}"; + } + + /** + * calculates the "checksum16" of the given string argument + */ + static String calculateChecksum16(String str) { + + byte[] bytes = str.getBytes(); + int sum = 0; + + // loop through each of the bytes and add them together + for (byte aByte : bytes) { + sum = sum + aByte; + } + + // calculate the "checksum16" + sum = sum % (int) Math.pow(2, 16); + + // return Integer.toHexString(sum); + return String.format("%04X", sum); + } + + static boolean validateResponse(final String header) { + + // // FIXME: 12.11.2016 whole response will be passed in + // final String patternString = "/\\{([0-9A-F]{2});FB;([0-9A-F]{2})/"; + // final Pattern pattern = Pattern.compile(patternString); + + // final Matcher matcher = pattern.matcher(header); + + // boolean matches = matcher.matches(); + // // return matches; + // // FIXME: 10.11.2016 + return true; + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxData.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxData.java new file mode 100644 index 0000000000000..953e1c631aca3 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxData.java @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.types.State; + +/** + * The {@link SolarMaxData} class is a POJO for storing the values returned from the SolarMax device and accessing the + * (decoded) values + * + * @author Jamie Townsend - Initial contribution + */ +public class SolarMaxData { + + private ZonedDateTime dataDateTime; + private boolean communicationSuccessful; + private final Map data = new HashMap<>(); + + public State getDataDateTime() { + return new DateTimeType(dataDateTime); + } + + public boolean has(SolarMaxCommandKey key) { + return data.containsKey(key); + } + + public State get(SolarMaxCommandKey key) { + switch (key) { + case SoftwareVersion: + return getSoftwareVersion(); + + case BuildNumber: + return getBuildNumber(); + + case Startups: + return getStartups(); + + case AcPhase1Current: + return getAcPhase1Current(); + + case AcPhase2Current: + return getAcPhase2Current(); + + case AcPhase3Current: + return getAcPhase3Current(); + + case EnergyGeneratedToday: + return getEnergyGeneratedToday(); + + case EnergyGeneratedTotal: + return getEnergyGeneratedTotal(); + + case OperatingHours: + return getOperatingHours(); + + case EnergyGeneratedYesterday: + return getEnergyGeneratedYesterday(); + + case EnergyGeneratedLastMonth: + return getEnergyGeneratedLastMonth(); + + case EnergyGeneratedLastYear: + return getEnergyGeneratedLastYear(); + + case EnergyGeneratedThisMonth: + return getEnergyGeneratedThisMonth(); + + case EnergyGeneratedThisYear: + return getEnergyGeneratedThisYear(); + + case CurrentPowerGenerated: + return getCurrentPowerGenerated(); + + case AcFrequency: + return getAcFrequency(); + + case AcPhase1Voltage: + return getAcPhase1Voltage(); + + case AcPhase2Voltage: + return getAcPhase2Voltage(); + + case AcPhase3Voltage: + return getAcPhase3Voltage(); + + case HeatSinkTemperature: + return getHeatSinkTemperature(); + + default: + return null; + } + } + + public void setDataDateTime(ZonedDateTime dataDateTime) { + this.dataDateTime = dataDateTime; + } + + public boolean wasCommunicationSuccessful() { + return this.communicationSuccessful; + } + + public void setCommunicationSuccessful(boolean communicationSuccessful) { + this.communicationSuccessful = communicationSuccessful; + } + + public DecimalType getSoftwareVersion() { + return getIntegerValueFrom(SolarMaxCommandKey.SoftwareVersion); + } + + public DecimalType getBuildNumber() { + return getIntegerValueFrom(SolarMaxCommandKey.BuildNumber); + } + + public DecimalType getStartups() { + return getIntegerValueFrom(SolarMaxCommandKey.Startups); + } + + public DecimalType getAcPhase1Current() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase1Current, 0.01); + } + + public DecimalType getAcPhase2Current() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase2Current, 0.01); + } + + public DecimalType getAcPhase3Current() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase3Current, 0.01); + } + + public DecimalType getEnergyGeneratedToday() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedToday, 100); + } + + public DecimalType getEnergyGeneratedTotal() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedTotal, 1000); + } + + public DecimalType getOperatingHours() { + return getIntegerValueFrom(SolarMaxCommandKey.OperatingHours); + } + + public DecimalType getEnergyGeneratedYesterday() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedYesterday, 100); + } + + public DecimalType getEnergyGeneratedLastMonth() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedLastMonth, 1000); + } + + public DecimalType getEnergyGeneratedLastYear() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedLastYear, 1000); + } + + public DecimalType getEnergyGeneratedThisMonth() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedThisMonth, 1000); + } + + public DecimalType getEnergyGeneratedThisYear() { + return getIntegerValueFrom(SolarMaxCommandKey.EnergyGeneratedThisYear, 1000); + } + + public DecimalType getCurrentPowerGenerated() { + return getIntegerValueFrom(SolarMaxCommandKey.CurrentPowerGenerated, 0.5); + } + + public DecimalType getAcFrequency() { + return getDecimalValueFrom(SolarMaxCommandKey.AcFrequency, 0.01); + } + + public DecimalType getAcPhase1Voltage() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase1Voltage, 0.1); + } + + public DecimalType getAcPhase2Voltage() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase2Voltage, 0.1); + } + + public DecimalType getAcPhase3Voltage() { + return getDecimalValueFrom(SolarMaxCommandKey.AcPhase3Voltage, 0.1); + } + + public DecimalType getHeatSinkTemperature() { + return getIntegerValueFrom(SolarMaxCommandKey.HeatSinkTemperature); + } + + private DecimalType getDecimalValueFrom(SolarMaxCommandKey solarMaxCommandKey, double multiplyByFactor) { + if (this.data.containsKey(solarMaxCommandKey)) { + String valueString = this.data.get(solarMaxCommandKey); + if (valueString != null) { + int valueInt = Integer.parseInt(valueString, 16); + return new DecimalType((float) valueInt * multiplyByFactor); + } + return null; + } + return null; + } + + private DecimalType getIntegerValueFrom(SolarMaxCommandKey solarMaxCommandKey, double multiplyByFactor) { + if (this.data.containsKey(solarMaxCommandKey)) { + String valueString = this.data.get(solarMaxCommandKey); + if (valueString != null) { + int valueInt = Integer.parseInt(valueString, 16); + return new DecimalType((int) (valueInt * multiplyByFactor)); + } + return null; + } + return null; + } + + private DecimalType getIntegerValueFrom(SolarMaxCommandKey solarMaxCommandKey) { + if (this.data.containsKey(solarMaxCommandKey)) { + String valueString = this.data.get(solarMaxCommandKey); + if (valueString != null) { + int valueInt = Integer.parseInt(valueString, 16); + return new DecimalType(valueInt); + } + return null; + } + return null; + } + + protected void setData(Map data) { + this.data.putAll(data); + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxException.java b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxException.java new file mode 100644 index 0000000000000..c4cd118338c49 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/connector/SolarMaxException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link SolarMaxException} Exception is used for general exceptions related to communications with the SolarMax + * device. + * + * @author Jamie Townsend - Initial contribution + */ +@NonNullByDefault +public class SolarMaxException extends Exception { + + private static final long serialVersionUID = 1L; + + public SolarMaxException(final String message, final Throwable cause) { + super(message, cause); + } + + public SolarMaxException(final Throwable cause) { + super(cause); + } + + public SolarMaxException(final String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..c92434fff9952 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + SolarMax Binding + This is the binding for SolarMax power inverters, particularly the MT Series + + diff --git a/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..883b9121f4832 --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,209 @@ + + + + + + + Basic thing for the SolarMax Power Inverter binding + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hostname or IP Address + + + + Port Number (defaults to 12345) + 12345 + + + + Refresh Interval in seconds (defaults to 15) + 15 + + + + + + + + + DateTime + + When was the data last read from the device + + + + + Number + + Software Version installed on the SolarMax device + + + + + Number + + Firmware Build Number installed on the SolarMax device + + + + + Number + + Number of times the device has started + + + + + Number + + Ac Phase 1 Current in Amps + + + + + Number + + Ac Phase 2 Current in Amps + + + + + Number + + Ac Phase 3 Current in Amps + + + + + Number + + Energy Generated Today in wH + + + + + Number + + Energy Generated Total since recording began in wH + + + + + Number + + Operating Hours since recording began in H + + + + + Number + + Energy Generated Yesterday in wH + + + + + Number + + Energy Generated Last Month in wH + + + + + Number + + Energy Generated Last Year in wH + + + + + Number + + Energy Generated This Month in wH + + + + + Number + + Energy Generated This Year in wH + + + + + Number + + Power currently being generated in w + + + + + Number + + AcFrequency in Hz + + + + + Number + + Ac Phase1 Voltage in V + + + + + Number + + Ac Phase2 Voltage in V + + + + + Number + + Ac Phase3 Voltage in V + + + + + Number + + Heat Sink Temperature in degrees celcius + + + + diff --git a/bundles/org.openhab.binding.solarmax/src/test/java/org/openhab/binding/solarmax/internal/connector/SolarMaxDataTest.java b/bundles/org.openhab.binding.solarmax/src/test/java/org/openhab/binding/solarmax/internal/connector/SolarMaxDataTest.java new file mode 100644 index 0000000000000..4fb3bb49933ec --- /dev/null +++ b/bundles/org.openhab.binding.solarmax/src/test/java/org/openhab/binding/solarmax/internal/connector/SolarMaxDataTest.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2021 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.solarmax.internal.connector; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; + +/** + * The {@link SolarMaxDataTest} class is used to test the {@link SolaMaxData} class. + * + * @author Johann Richard - Initial contribution + */ +@NonNullByDefault +public class SolarMaxDataTest { + + @Test + public void gettersTest() throws Exception { + + // SolarMaxData solarMaxData = new SolarMaxData().dataDateTime() + } + + @Test + public void dataDateTimeGetterSetterTest() throws Exception { + + // dataDateTime shouldn't be a problem, but check it anyway + ZonedDateTime dateTimeOriginal = ZonedDateTime.now(); + ZonedDateTime dateTimeUpdated = dateTimeOriginal.plusDays(2); + + SolarMaxData solarMaxData = new SolarMaxData(); + + solarMaxData.setDataDateTime(dateTimeOriginal); + assertEquals(new DateTimeType(dateTimeOriginal), solarMaxData.getDataDateTime()); + + solarMaxData.setDataDateTime(dateTimeUpdated); + assertEquals(new DateTimeType(dateTimeUpdated), solarMaxData.getDataDateTime()); + } + + @Test + public void valueGetterSetterTest() throws Exception { + + String softwareVersionOriginal = "3B8B"; // 15243 in hex + String softwareVersionUpdated = "3B8C"; // 15244 in hex + + SolarMaxData solarMaxData = new SolarMaxData(); + + Map dataOrig = new HashMap<>(); + dataOrig.put(SolarMaxCommandKey.SoftwareVersion, softwareVersionOriginal); + solarMaxData.setData(dataOrig); + DecimalType origVersion = solarMaxData.get(SolarMaxCommandKey.SoftwareVersion).as(DecimalType.class); + assertNotNull(origVersion); + assertEquals(Integer.parseInt(softwareVersionOriginal, 16), origVersion.intValue()); + + Map dataUpdated = new HashMap<>(); + dataUpdated.put(SolarMaxCommandKey.SoftwareVersion, softwareVersionUpdated); + solarMaxData.setData(dataUpdated); + DecimalType updatedVersion = solarMaxData.get(SolarMaxCommandKey.SoftwareVersion).as(DecimalType.class); + assertNotEquals(origVersion, updatedVersion); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 3c6b37c4875a2..0b1fc755a0cb8 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -1,574 +1,1124 @@ - - + + 4.0.0 - + + org.openhab.addons + org.openhab.addons.reactor + 3.1.0-SNAPSHOT + - + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + pom - + openHAB Add-ons :: Bundles - + + + org.openhab.automation.groovyscripting + org.openhab.automation.jythonscripting + org.openhab.automation.pidcontroller + + org.openhab.io.homekit + org.openhab.io.hueemulation + org.openhab.io.imperihome + org.openhab.io.neeo + org.openhab.io.openhabcloud + + org.openhab.transform.bin2json + org.openhab.transform.exec + org.openhab.transform.javascript + org.openhab.transform.jinja + org.openhab.transform.jsonpath + org.openhab.transform.map + org.openhab.transform.regex + org.openhab.transform.scale + org.openhab.transform.xpath + org.openhab.transform.xslt + + org.openhab.binding.adorne + org.openhab.binding.airquality + org.openhab.binding.airvisualnode + org.openhab.binding.alarmdecoder + org.openhab.binding.allplay + org.openhab.binding.amazondashbutton + org.openhab.binding.amazonechocontrol + org.openhab.binding.ambientweather + org.openhab.binding.androiddebugbridge + org.openhab.binding.astro + org.openhab.binding.atlona + org.openhab.binding.autelis + org.openhab.binding.automower + org.openhab.binding.avmfritz + org.openhab.binding.bigassfan + org.openhab.binding.bluetooth + org.openhab.binding.bluetooth.airthings + org.openhab.binding.bluetooth.am43 + org.openhab.binding.bluetooth.bluegiga + org.openhab.binding.bluetooth.bluez + org.openhab.binding.bluetooth.blukii + org.openhab.binding.bluetooth.daikinmadoka + org.openhab.binding.bluetooth.enoceanble + org.openhab.binding.bluetooth.generic + org.openhab.binding.bluetooth.govee + org.openhab.binding.bluetooth.roaming + org.openhab.binding.bluetooth.ruuvitag + org.openhab.binding.boschindego + org.openhab.binding.boschshc + org.openhab.binding.bosesoundtouch + org.openhab.binding.broadlinkthermostat + org.openhab.binding.bsblan + org.openhab.binding.bticinosmarther + org.openhab.binding.buienradar + org.openhab.binding.caddx + org.openhab.binding.cbus + org.openhab.binding.chromecast + org.openhab.binding.cm11a + org.openhab.binding.comfoair + org.openhab.binding.coolmasternet + org.openhab.binding.coronastats + org.openhab.binding.daikin + org.openhab.binding.danfossairunit + org.openhab.binding.darksky + org.openhab.binding.deconz + org.openhab.binding.denonmarantz + org.openhab.binding.digiplex + org.openhab.binding.digitalstrom + org.openhab.binding.dlinksmarthome + org.openhab.binding.dmx + org.openhab.binding.doorbird + org.openhab.binding.draytonwiser + org.openhab.binding.dscalarm + org.openhab.binding.dsmr + org.openhab.binding.dwdpollenflug + org.openhab.binding.dwdunwetter + org.openhab.binding.ecobee + org.openhab.binding.elerotransmitterstick + org.openhab.binding.energenie + org.openhab.binding.enigma2 + org.openhab.binding.enocean + org.openhab.binding.enturno + org.openhab.binding.epsonprojector + org.openhab.binding.etherrain + org.openhab.binding.evohome + org.openhab.binding.exec + org.openhab.binding.feed + org.openhab.binding.feican + org.openhab.binding.fmiweather + org.openhab.binding.folderwatcher + org.openhab.binding.folding + org.openhab.binding.foobot + org.openhab.binding.freebox + org.openhab.binding.fronius + org.openhab.binding.fsinternetradio + org.openhab.binding.ftpupload + org.openhab.binding.gardena + org.openhab.binding.gce + org.openhab.binding.generacmobilelink + org.openhab.binding.goecharger + org.openhab.binding.gpio + org.openhab.binding.globalcache + org.openhab.binding.gpstracker + org.openhab.binding.gree + org.openhab.binding.groheondus + org.openhab.binding.harmonyhub + org.openhab.binding.haywardomnilogic + org.openhab.binding.hdanywhere + org.openhab.binding.hdpowerview + org.openhab.binding.helios + org.openhab.binding.heliosventilation + org.openhab.binding.heos + org.openhab.binding.homematic + org.openhab.binding.hpprinter + org.openhab.binding.http + org.openhab.binding.hue + org.openhab.binding.hydrawise + org.openhab.binding.hyperion + org.openhab.binding.iammeter + org.openhab.binding.iaqualink + org.openhab.binding.icalendar + org.openhab.binding.icloud + org.openhab.binding.ihc + org.openhab.binding.innogysmarthome + org.openhab.binding.insteon + org.openhab.binding.ipcamera + org.openhab.binding.intesis + org.openhab.binding.ipp + org.openhab.binding.irobot + org.openhab.binding.irtrans + org.openhab.binding.ism8 + org.openhab.binding.jablotron + org.openhab.binding.jeelink + org.openhab.binding.kaleidescape + org.openhab.binding.keba + org.openhab.binding.km200 + org.openhab.binding.knx + org.openhab.binding.kodi + org.openhab.binding.konnected + org.openhab.binding.kostalinverter + org.openhab.binding.kvv + org.openhab.binding.lametrictime + org.openhab.binding.lcn + org.openhab.binding.leapmotion + org.openhab.binding.lghombot + org.openhab.binding.lgtvserial + org.openhab.binding.lgwebos + org.openhab.binding.lifx + org.openhab.binding.linky + org.openhab.binding.linuxinput + org.openhab.binding.lirc + org.openhab.binding.logreader + org.openhab.binding.loxone + org.openhab.binding.luftdateninfo + org.openhab.binding.lutron + org.openhab.binding.magentatv + org.openhab.binding.mail + org.openhab.binding.max + org.openhab.binding.mcp23017 + org.openhab.binding.melcloud + org.openhab.binding.meteoalerte + org.openhab.binding.meteoblue + org.openhab.binding.meteostick + org.openhab.binding.miele + org.openhab.binding.mihome + org.openhab.binding.miio + org.openhab.binding.millheat + org.openhab.binding.milight + org.openhab.binding.minecraft + org.openhab.binding.modbus + org.openhab.binding.modbus.e3dc + org.openhab.binding.modbus.sbc + org.openhab.binding.modbus.studer + org.openhab.binding.modbus.sunspec + org.openhab.binding.modbus.stiebeleltron + org.openhab.binding.modbus.helioseasycontrols + org.openhab.binding.monopriceaudio + org.openhab.binding.mpd + org.openhab.binding.mqtt + org.openhab.binding.mqtt.espmilighthub + org.openhab.binding.mqtt.generic + org.openhab.binding.mqtt.homeassistant + org.openhab.binding.mqtt.homie + org.openhab.binding.myq + org.openhab.binding.mystrom + org.openhab.binding.nanoleaf + org.openhab.binding.neato + org.openhab.binding.neeo + org.openhab.binding.neohub + org.openhab.binding.nest + org.openhab.binding.netatmo + org.openhab.binding.network + org.openhab.binding.networkupstools + org.openhab.binding.nibeheatpump + org.openhab.binding.nibeuplink + org.openhab.binding.nikobus + org.openhab.binding.nikohomecontrol + org.openhab.binding.novafinedust + org.openhab.binding.ntp + org.openhab.binding.nuki + org.openhab.binding.nuvo + org.openhab.binding.nzwateralerts + org.openhab.binding.oceanic + org.openhab.binding.ojelectronics + org.openhab.binding.omnikinverter + org.openhab.binding.omnilink + org.openhab.binding.onebusaway + org.openhab.binding.onewiregpio + org.openhab.binding.onewire + org.openhab.binding.onkyo + org.openhab.binding.opengarage + org.openhab.binding.opensprinkler + org.openhab.binding.openthermgateway + org.openhab.binding.openuv + org.openhab.binding.openweathermap + org.openhab.binding.openwebnet + org.openhab.binding.oppo + org.openhab.binding.orvibo + org.openhab.binding.paradoxalarm + org.openhab.binding.pentair + org.openhab.binding.phc + org.openhab.binding.pilight + org.openhab.binding.pioneeravr + org.openhab.binding.pixometer + org.openhab.binding.pjlinkdevice + org.openhab.binding.playstation + org.openhab.binding.plclogo + org.openhab.binding.plugwise + org.openhab.binding.powermax + org.openhab.binding.pulseaudio + org.openhab.binding.pushbullet + org.openhab.binding.pushover + org.openhab.binding.radiothermostat + org.openhab.binding.regoheatpump + org.openhab.binding.revogi + org.openhab.binding.remoteopenhab + org.openhab.binding.rfxcom + org.openhab.binding.rme + org.openhab.binding.robonect + org.openhab.binding.roku + org.openhab.binding.rotel + org.openhab.binding.russound + org.openhab.binding.sagercaster + org.openhab.binding.samsungtv + org.openhab.binding.satel + org.openhab.binding.senechome + org.openhab.binding.seneye + org.openhab.binding.sensebox + org.openhab.binding.sensibo + org.openhab.binding.serial + org.openhab.binding.serialbutton + org.openhab.binding.shelly + org.openhab.binding.silvercrestwifisocket + org.openhab.binding.siemensrds + org.openhab.binding.sinope + org.openhab.binding.sleepiq + org.openhab.binding.smaenergymeter + org.openhab.binding.smartmeter + org.openhab.binding.smhi + org.openhab.binding.smartthings + org.openhab.binding.snmp + org.openhab.binding.solaredge + org.openhab.binding.solarlog + org.openhab.binding.solarmax + org.openhab.binding.somfymylink + org.openhab.binding.somfytahoma + org.openhab.binding.sonos + org.openhab.binding.sonyaudio + org.openhab.binding.sonyprojector + org.openhab.binding.spotify + org.openhab.binding.squeezebox + org.openhab.binding.surepetcare + org.openhab.binding.synopanalyzer + org.openhab.binding.systeminfo + org.openhab.binding.tacmi + org.openhab.binding.tado + org.openhab.binding.tankerkoenig + org.openhab.binding.telegram + org.openhab.binding.teleinfo + org.openhab.binding.tellstick + org.openhab.binding.tesla + org.openhab.binding.tibber + org.openhab.binding.tivo + org.openhab.binding.touchwand + org.openhab.binding.tplinksmarthome + org.openhab.binding.tr064 + org.openhab.binding.tradfri + org.openhab.binding.unifi + org.openhab.binding.unifiedremote + org.openhab.binding.upnpcontrol + org.openhab.binding.upb + org.openhab.binding.urtsi + org.openhab.binding.valloxmv + org.openhab.binding.vektiva + org.openhab.binding.velbus + org.openhab.binding.velux + org.openhab.binding.venstarthermostat + org.openhab.binding.verisure + org.openhab.binding.vigicrues + org.openhab.binding.vitotronic + org.openhab.binding.volvooncall + org.openhab.binding.weathercompany + org.openhab.binding.weatherunderground + org.openhab.binding.wemo + org.openhab.binding.wifiled + org.openhab.binding.windcentrale + org.openhab.binding.wlanthermo + org.openhab.binding.wled + org.openhab.binding.xmltv + org.openhab.binding.xmppclient + org.openhab.binding.yamahareceiver + org.openhab.binding.yioremote + org.openhab.binding.yeelight + org.openhab.binding.zoneminder + org.openhab.binding.zway + + org.openhab.persistence.dynamodb + org.openhab.persistence.influxdb + org.openhab.persistence.jdbc + org.openhab.persistence.jpa + org.openhab.persistence.mapdb + org.openhab.persistence.mongodb + org.openhab.persistence.rrd4j + + org.openhab.voice.googletts + org.openhab.voice.mactts + org.openhab.voice.marytts + org.openhab.voice.picotts + org.openhab.voice.pollytts + org.openhab.voice.voicerss + + - + + target/dependency + + - + + + org.lastnpe.eea + eea-all + ${eea.version} + + + + org.openhab.core.bom + org.openhab.core.bom.compile + pom + provided + + + org.openhab.core.bom + org.openhab.core.bom.openhab-core + pom + provided + + + commons-net + commons-net + + + + + org.openhab.core.bom + org.openhab.core.bom.test + pom + test + + + + org.apache.karaf.features + framework + ${karaf.version} + kar + true + + + * + * + + + + + + org.apache.karaf.features + standard + ${karaf.version} + features + xml + provided + + - + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + true + + + + org.apache.karaf.tooling + karaf-maven-plugin + ${karaf.version} + true + + 80 + true + true + false + true + true + + + + compile + + features-generate-descriptor + + generate-resources + + ${feature.directory} + + + + karaf-feature-verification + + verify + + verify + + + + mvn:org.apache.karaf.features/framework/${karaf.version}/xml/features + mvn:org.apache.karaf.features/standard/${karaf.version}/xml/features + + file:${project.build.directory}/feature/feature.xml + + org.apache.karaf.features:framework + ${oh.java.version} + + framework + + + openhab-* + + false + true + first + + + + + + - + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.karaf.tooling + karaf-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + embed-dependencies + + unpack-dependencies + + + runtime + jar + javax.activation,org.apache.karaf.features,org.lastnpe.eea + ${dep.noembedding} + ${project.build.directory}/classes + true + true + true + jar + + + + unpack-eea + + unpack + + + + + org.lastnpe.eea + eea-all + ${eea.version} + true + + + + + + + + - + + + + no-embed-dependencies + + + noEmbedDependencies.profile + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + embed-dependencies + none + + + + + + + - +