[solarmax] Initial contribution (#10414)
* SolarMax Binding Initial implementation Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * #10413 camelCaserizeTheChannelNames Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * #10413 Delete commented code and Refactor Brute Force Command Discovery into something commitable Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * 10413 Delete commented code and Refactor Brute Force Command Discovery into something commitable Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * #10413 Codestyle Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * 10413 Codestyle Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * #10413 corrected sat-plugin errors Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * #10413 updates from code reviews in PR #10414 Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * 10413 mvn spotless:apply Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * 10413 Updated to 3.2.0-SNAPSHOT Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Fixed conflicts introduced by foreign commit. Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Updated copyright years Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Ran mvn spotless:apply to resolve formatting issues Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Updates from review Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Switch to using Units & move softwareVersion & buildNumber to properties Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * A couple of review related updates Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * A couple more review related changes. Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Added Full Example to README.md Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Update parent pom.xml version Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> * Update bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandlerFactory.java Signed-off-by: Fabian Wolter <github@fabian-wolter.de> * Update bundles/org.openhab.binding.solarmax/src/main/java/org/openhab/binding/solarmax/internal/SolarMaxHandlerFactory.java Signed-off-by: Fabian Wolter <github@fabian-wolter.de> Signed-off-by: Jamie Townsend <jamie_townsend@hotmail.com> Signed-off-by: Fabian Wolter <github@fabian-wolter.de> Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.solarmax-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-solarmax" description="SolarMax Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.solarmax/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.solarmax.internal;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.solarmax.internal.connector.SolarMaxCommandKey;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* The {@link SolarMaxChannel} Enum defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jamie Townsend - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum SolarMaxChannel {
|
||||
|
||||
CHANNEL_LAST_UPDATED("lastUpdated", null), //
|
||||
CHANNEL_STARTUPS(SolarMaxCommandKey.startups.name(), null),
|
||||
CHANNEL_AC_PHASE1_CURRENT(SolarMaxCommandKey.acPhase1Current.name(), Units.AMPERE),
|
||||
CHANNEL_AC_PHASE2_CURRENT(SolarMaxCommandKey.acPhase2Current.name(), Units.AMPERE),
|
||||
CHANNEL_AC_PHASE3_CURRENT(SolarMaxCommandKey.acPhase3Current.name(), Units.AMPERE),
|
||||
CHANNEL_ENERGY_GENERATED_TODAY(SolarMaxCommandKey.energyGeneratedToday.name(), Units.WATT_HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_TOTAL(SolarMaxCommandKey.energyGeneratedTotal.name(), Units.WATT_HOUR),
|
||||
CHANNEL_OPERATING_HOURS(SolarMaxCommandKey.operatingHours.name(), Units.HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_YESTERDAY(SolarMaxCommandKey.energyGeneratedYesterday.name(), Units.WATT_HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_LAST_MONTH(SolarMaxCommandKey.energyGeneratedLastMonth.name(), Units.WATT_HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_LAST_YEAR(SolarMaxCommandKey.energyGeneratedLastYear.name(), Units.WATT_HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_THIS_MONTH(SolarMaxCommandKey.energyGeneratedThisMonth.name(), Units.WATT_HOUR),
|
||||
CHANNEL_ENERGY_GENERATED_THIS_YEAR(SolarMaxCommandKey.energyGeneratedThisYear.name(), Units.WATT_HOUR),
|
||||
CHANNEL_CURRENT_POWER_GENERATED(SolarMaxCommandKey.currentPowerGenerated.name(), Units.WATT_HOUR),
|
||||
CHANNEL_AC_FREQUENCY(SolarMaxCommandKey.acFrequency.name(), Units.HERTZ),
|
||||
CHANNEL_AC_PHASE1_VOLTAGE(SolarMaxCommandKey.acPhase1Voltage.name(), Units.VOLT),
|
||||
CHANNEL_AC_PHASE2_VOLTAGE(SolarMaxCommandKey.acPhase2Voltage.name(), Units.VOLT),
|
||||
CHANNEL_AC_PHASE3_VOLTAGE(SolarMaxCommandKey.acPhase3Voltage.name(), Units.VOLT),
|
||||
CHANNEL_HEAT_SINK_TEMPERATUR(SolarMaxCommandKey.heatSinkTemperature.name(), SIUnits.CELSIUS);
|
||||
|
||||
private final String channelId;
|
||||
|
||||
@Nullable
|
||||
private Unit<?> unit;
|
||||
|
||||
private SolarMaxChannel(String channelId, @Nullable Unit<?> unit) {
|
||||
this.channelId = channelId;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public String getChannelId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Unit<?> getUnit() {
|
||||
return this.unit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.solarmax.internal;
|
||||
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
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.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
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
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SolarMaxHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SolarMaxHandler.class);
|
||||
|
||||
private SolarMaxConfiguration config = getConfigAs(SolarMaxConfiguration.class);
|
||||
|
||||
@Nullable
|
||||
private ScheduledFuture<?> pollingJob;
|
||||
|
||||
public SolarMaxHandler(final Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
// Read only
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
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() {
|
||||
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()) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateProperties(solarMaxData);
|
||||
updateChannels(solarMaxData);
|
||||
return;
|
||||
}
|
||||
} catch (SolarMaxException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Communication error with the device: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the channels
|
||||
*/
|
||||
private void updateChannels(SolarMaxData solarMaxData) {
|
||||
logger.debug("Updating all channels");
|
||||
for (SolarMaxChannel solarMaxChannel : SolarMaxChannel.values()) {
|
||||
String channelId = solarMaxChannel.getChannelId();
|
||||
Channel channel = getThing().getChannel(channelId);
|
||||
|
||||
if (channelId.equals(SolarMaxChannel.CHANNEL_LAST_UPDATED.getChannelId())) {
|
||||
// CHANNEL_LAST_UPDATED shows when the device was last read and does not come from the device, 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 = convertValueToState(solarMaxData.get(SolarMaxCommandKey.valueOf(channelId)),
|
||||
solarMaxChannel.getUnit());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable State convertValueToState(Number value, @Nullable Unit<?> unit) {
|
||||
if (unit == null) {
|
||||
return new DecimalType(value.floatValue());
|
||||
}
|
||||
return new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the properties
|
||||
*/
|
||||
private void updateProperties(SolarMaxData solarMaxData) {
|
||||
logger.debug("Updating properties");
|
||||
for (SolarMaxProperty solarMaxProperty : SolarMaxProperty.values()) {
|
||||
String propertyId = solarMaxProperty.getPropertyId();
|
||||
Number valNumber = solarMaxData.get(SolarMaxCommandKey.valueOf(propertyId));
|
||||
if (valNumber == null) {
|
||||
logger.debug("Null value returned for value of {}: {}", getThing().getUID(), propertyId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// deal with properties
|
||||
if (propertyId.equals(SolarMaxProperty.PROPERTY_BUILD_NUMBER.getPropertyId())
|
||||
|| propertyId.equals(SolarMaxProperty.PROPERTY_SOFTWARE_VERSION.getPropertyId())) {
|
||||
updateProperty(solarMaxProperty.getPropertyId(), valNumber.toString());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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;
|
||||
|
||||
/**
|
||||
* 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 static final Set<ThingTypeUID> 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)) {
|
||||
return new SolarMaxHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.solarmax.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.solarmax.internal.connector.SolarMaxCommandKey;
|
||||
|
||||
/**
|
||||
* The {@link SolarMaxProperty} Enum defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Jamie Townsend - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum SolarMaxProperty {
|
||||
|
||||
PROPERTY_SOFTWARE_VERSION(SolarMaxCommandKey.softwareVersion.name()),
|
||||
PROPERTY_BUILD_NUMBER(SolarMaxCommandKey.buildNumber.name());
|
||||
|
||||
private final String propertyId;
|
||||
|
||||
private SolarMaxProperty(String propertyId) {
|
||||
this.propertyId = propertyId;
|
||||
}
|
||||
|
||||
public String getPropertyId() {
|
||||
return propertyId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.solarmax.internal.connector;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link SolarMaxCommandKey} enum defines the commands that are understood by the SolarMax device
|
||||
*
|
||||
* @author Jamie Townsend - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum SolarMaxCommandKey {
|
||||
// for further commands, that are not implemented here, see this binding's README.md file
|
||||
|
||||
// Valid commands which returned a non-null value during testing
|
||||
buildNumber("BDN"), //
|
||||
startups("CAC"), //
|
||||
acPhase1Current("IL1"), //
|
||||
acPhase2Current("IL2"), //
|
||||
acPhase3Current("IL3"), //
|
||||
energyGeneratedToday("KDY"), //
|
||||
operatingHours("KHR"), //
|
||||
energyGeneratedYesterday("KLD"), //
|
||||
energyGeneratedLastMonth("KLM"), //
|
||||
energyGeneratedLastYear("KLY"), //
|
||||
energyGeneratedThisMonth("KMT"), //
|
||||
energyGeneratedTotal("KT0"), //
|
||||
energyGeneratedThisYear("KYR"), //
|
||||
currentPowerGenerated("PAC"), //
|
||||
softwareVersion("SWV"), //
|
||||
heatSinkTemperature("TKK"), //
|
||||
acFrequency("TNF"), //
|
||||
acPhase1Voltage("UL1"), //
|
||||
acPhase2Voltage("UL2"), //
|
||||
acPhase3Voltage("UL3"), //
|
||||
UNKNOWN("UNKNOWN") // really unknown - shouldn't ever be sent to the device
|
||||
;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SolarMaxConnector {
|
||||
|
||||
/**
|
||||
* default port number of SolarMax devices is...
|
||||
*/
|
||||
private static final int DEFAULT_PORT = 12345;
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SolarMaxConnector.class);
|
||||
|
||||
/**
|
||||
* default timeout for socket connections is 1 second
|
||||
*/
|
||||
private static final int CONNECTION_TIMEOUT = 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<SolarMaxCommandKey> 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<SolarMaxCommandKey, @Nullable String> 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<SolarMaxCommandKey, @Nullable String> getValuesFromSolarMax(final String host, int port,
|
||||
final List<SolarMaxCommandKey> commandList) throws SolarMaxException {
|
||||
Socket socket;
|
||||
|
||||
Map<SolarMaxCommandKey, @Nullable String> 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<SolarMaxCommandKey> 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;
|
||||
}
|
||||
|
||||
static String getCommandString(List<SolarMaxCommandKey> commandList) {
|
||||
String commandString = "";
|
||||
for (SolarMaxCommandKey command : commandList) {
|
||||
if (!commandString.isEmpty()) {
|
||||
commandString = commandString + ";";
|
||||
}
|
||||
commandString = commandString + command.getCommandKey();
|
||||
}
|
||||
return commandString;
|
||||
}
|
||||
|
||||
private static Map<SolarMaxCommandKey, @Nullable String> getValuesFromSolarMax(final Socket socket,
|
||||
final List<SolarMaxCommandKey> 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<SolarMaxCommandKey, @Nullable String> getValuesFromSolarMax(final OutputStream outputStream,
|
||||
final InputStream inputStream, final List<SolarMaxCommandKey> commandList) throws SolarMaxException {
|
||||
Map<SolarMaxCommandKey, @Nullable String> returnedValues;
|
||||
String commandString = getCommandString(commandList);
|
||||
String request = contructRequest(commandString);
|
||||
try {
|
||||
LOGGER.trace(" ==>: {}", request);
|
||||
|
||||
outputStream.write(request.getBytes());
|
||||
|
||||
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<SolarMaxCommandKey, @Nullable String> extractValuesFromResponse(String response) {
|
||||
final Map<SolarMaxCommandKey, @Nullable String> 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), CONNECTION_TIMEOUT);
|
||||
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 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 = "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) {
|
||||
// probably should implement a patter matcher with a patternString like "/\\{([0-9A-F]{2});FB;([0-9A-F]{2})/",
|
||||
// but for now...
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.solarmax.internal.connector;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.DefaultLocation;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
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
|
||||
*/
|
||||
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.FIELD, DefaultLocation.TYPE_BOUND,
|
||||
DefaultLocation.TYPE_ARGUMENT })
|
||||
public class SolarMaxData {
|
||||
|
||||
private ZonedDateTime dataDateTime = ZonedDateTime.now();
|
||||
|
||||
private boolean communicationSuccessful;
|
||||
|
||||
private final Map<SolarMaxCommandKey, @Nullable String> data = new HashMap<>();
|
||||
|
||||
public State getDataDateTime() {
|
||||
return new DateTimeType(dataDateTime);
|
||||
}
|
||||
|
||||
public boolean has(SolarMaxCommandKey key) {
|
||||
return data.containsKey(key);
|
||||
}
|
||||
|
||||
public Number 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 Number getSoftwareVersion() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.softwareVersion);
|
||||
}
|
||||
|
||||
public Number getBuildNumber() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.buildNumber);
|
||||
}
|
||||
|
||||
public Number getStartups() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.startups);
|
||||
}
|
||||
|
||||
public Number getAcPhase1Current() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase1Current, 0.01);
|
||||
}
|
||||
|
||||
public Number getAcPhase2Current() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase2Current, 0.01);
|
||||
}
|
||||
|
||||
public Number getAcPhase3Current() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase3Current, 0.01);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedToday() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedToday, 100);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedTotal() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedTotal, 1000);
|
||||
}
|
||||
|
||||
public Number getOperatingHours() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.operatingHours);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedYesterday() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedYesterday, 100);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedLastMonth() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedLastMonth, 1000);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedLastYear() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedLastYear, 1000);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedThisMonth() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedThisMonth, 1000);
|
||||
}
|
||||
|
||||
public Number getEnergyGeneratedThisYear() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.energyGeneratedThisYear, 1000);
|
||||
}
|
||||
|
||||
public Number getCurrentPowerGenerated() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.currentPowerGenerated, 0.5);
|
||||
}
|
||||
|
||||
Number getAcFrequency() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acFrequency, 0.01);
|
||||
}
|
||||
|
||||
public Number getAcPhase1Voltage() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase1Voltage, 0.1);
|
||||
}
|
||||
|
||||
public Number getAcPhase2Voltage() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase2Voltage, 0.1);
|
||||
}
|
||||
|
||||
public Number getAcPhase3Voltage() {
|
||||
return getDecimalValueFrom(SolarMaxCommandKey.acPhase3Voltage, 0.1);
|
||||
}
|
||||
|
||||
public Number getHeatSinkTemperature() {
|
||||
return getIntegerValueFrom(SolarMaxCommandKey.heatSinkTemperature);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Number 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 (float) valueInt * multiplyByFactor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Number 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 (int) (valueInt * multiplyByFactor);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Number getIntegerValueFrom(SolarMaxCommandKey solarMaxCommandKey) {
|
||||
if (this.data.containsKey(solarMaxCommandKey)) {
|
||||
String valueString = this.data.get(solarMaxCommandKey);
|
||||
if (valueString != null) {
|
||||
return Integer.parseInt(valueString, 16);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void setData(Map<SolarMaxCommandKey, @Nullable String> data) {
|
||||
this.data.putAll(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="solarmax" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>SolarMax Binding</name>
|
||||
<description>This is the binding for SolarMax power inverters, particularly the MT Series</description>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,186 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="solarmax"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- SolarMaxBindingConstants.THING_TYPE_ID -->
|
||||
<thing-type id="inverter">
|
||||
<label>SolarMax Power Inverter</label>
|
||||
<description>Basic thing for the SolarMax Power Inverter binding</description>
|
||||
|
||||
<channels>
|
||||
<channel id="lastUpdated" typeId="lastUpdated"/>
|
||||
<channel id="startups" typeId="startups"/>
|
||||
<channel id="acPhase1Current" typeId="acPhase1Current"/>
|
||||
<channel id="acPhase2Current" typeId="acPhase2Current"/>
|
||||
<channel id="acPhase3Current" typeId="acPhase3Current"/>
|
||||
<channel id="energyGeneratedToday" typeId="energyGeneratedToday"/>
|
||||
<channel id="energyGeneratedTotal" typeId="energyGeneratedTotal"/>
|
||||
<channel id="operatingHours" typeId="operatingHours"/>
|
||||
<channel id="energyGeneratedYesterday" typeId="energyGeneratedYesterday"/>
|
||||
<channel id="energyGeneratedLastMonth" typeId="energyGeneratedLastMonth"/>
|
||||
<channel id="energyGeneratedLastYear" typeId="energyGeneratedLastYear"/>
|
||||
<channel id="energyGeneratedThisMonth" typeId="energyGeneratedThisMonth"/>
|
||||
<channel id="energyGeneratedThisYear" typeId="energyGeneratedThisYear"/>
|
||||
<channel id="currentPowerGenerated" typeId="currentPowerGenerated"/>
|
||||
<channel id="acFrequency" typeId="acFrequency"/>
|
||||
<channel id="acPhase1Voltage" typeId="acPhase1Voltage"/>
|
||||
<channel id="acPhase2Voltage" typeId="acPhase2Voltage"/>
|
||||
<channel id="acPhase3Voltage" typeId="acPhase3Voltage"/>
|
||||
<channel id="heatSinkTemperature" typeId="heatSinkTemperature"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Host</label>
|
||||
<description>Hostname or IP Address</description>
|
||||
</parameter>
|
||||
<parameter name="portNumber" type="integer" required="false">
|
||||
<label>Port</label>
|
||||
<description>Port Number (defaults to 12345)</description>
|
||||
<default>12345</default>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" required="false">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Refresh Interval in seconds (defaults to 15)</description>
|
||||
<default>15</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="lastUpdated">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Updated</label>
|
||||
<description>Time when data was last read from the device</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="startups">
|
||||
<item-type>Number</item-type>
|
||||
<label>Startups</label>
|
||||
<description>Number of times the device has started</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase1Current">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>AC Phase 1 Current</label>
|
||||
<description>AC Phase 1 Current in Amps</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase2Current">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>AC Phase 2 Current</label>
|
||||
<description>AC Phase 2 Current in Amps</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase3Current">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>AC Phase 3 Current</label>
|
||||
<description>AC Phase 3 Current in Amps</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedToday">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated Today</label>
|
||||
<description>Energy Generated Today in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedTotal">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated Total</label>
|
||||
<description>Energy Generated Total since recording began in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="operatingHours">
|
||||
<item-type>Number</item-type>
|
||||
<label>Operating Hours</label>
|
||||
<description>Operating Hours since recording began in H</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedYesterday">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated Yesterday</label>
|
||||
<description>Energy Generated Yesterday in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedLastMonth">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated Last Month</label>
|
||||
<description>Energy Generated Last Month in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedLastYear">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated Last Year</label>
|
||||
<description>Energy Generated Last Year in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedThisMonth">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated This Month</label>
|
||||
<description>Energy Generated This Month in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyGeneratedThisYear">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Energy Generated This Year</label>
|
||||
<description>Energy Generated This Year in wH</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="currentPowerGenerated">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Current Power Generated</label>
|
||||
<description>Power currently being generated in w</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acFrequency">
|
||||
<item-type>Number:Frequency</item-type>
|
||||
<label>AC Frequency</label>
|
||||
<description>AcFrequency in Hz</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase1Voltage">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>AC Phase1 Voltage</label>
|
||||
<description>AC Phase1 Voltage in V</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase2Voltage">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>AC Phase2 Voltage</label>
|
||||
<description>AC Phase2 Voltage in V</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="acPhase3Voltage">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>AC Phase3 Voltage</label>
|
||||
<description>AC Phase3 Voltage in V</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="heatSinkTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Heat Sink Temperature</label>
|
||||
<description>Heat Sink Temperature in degrees celcius</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
|
||||
/**
|
||||
* The {@link SolarMaxDataTest} class is used to test the {@link SolaMaxData} class.
|
||||
*
|
||||
* @author Jamie Townsend - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SolarMaxDataTest {
|
||||
|
||||
@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 startupsOriginal = "3B8B"; // 15243 in hex
|
||||
String startupsUpdated = "3B8C"; // 15244 in hex
|
||||
|
||||
SolarMaxData solarMaxData = new SolarMaxData();
|
||||
|
||||
Map<SolarMaxCommandKey, @Nullable String> dataOrig = new HashMap<>();
|
||||
dataOrig.put(SolarMaxCommandKey.startups, startupsOriginal);
|
||||
solarMaxData.setData(dataOrig);
|
||||
|
||||
@Nullable
|
||||
Number origVersion = solarMaxData.get(SolarMaxCommandKey.startups);
|
||||
|
||||
assertNotNull(origVersion);
|
||||
assertEquals(Integer.parseInt(startupsOriginal, 16), origVersion.intValue());
|
||||
|
||||
Map<SolarMaxCommandKey, @Nullable String> dataUpdated = new HashMap<>();
|
||||
dataUpdated.put(SolarMaxCommandKey.startups, startupsUpdated);
|
||||
solarMaxData.setData(dataUpdated);
|
||||
Number updatedVersion = solarMaxData.get(SolarMaxCommandKey.startups);
|
||||
assertNotEquals(origVersion, updatedVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.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.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SolarmaxConnectorFindCommands} class wass used to brute-force detect different replies from the SolarMax
|
||||
* device
|
||||
*
|
||||
* @author Jamie Townsend - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SolarmaxConnectorFindCommands {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(SolarMaxConnector.class);
|
||||
|
||||
private static final String HOST = "192.168.1.151";
|
||||
private static final int PORT = 12345;
|
||||
private static final int CONNECTION_TIMEOUT = 1000; // ms
|
||||
|
||||
@Test
|
||||
public void testForCommands() throws UnknownHostException, SolarMaxException {
|
||||
List<String> validCommands = new ArrayList<>();
|
||||
List<String> commandsToCheck = new ArrayList<String>();
|
||||
List<String> failedCommands = new ArrayList<>();
|
||||
int failedCommandRetry = 0;
|
||||
String lastFailedCommand = "";
|
||||
|
||||
for (String first : getCharacters()) {
|
||||
for (String second : getCharacters()) {
|
||||
for (String third : getCharacters()) {
|
||||
commandsToCheck.add(first + second + third);
|
||||
|
||||
// specifically searching for "E" errors with 4 characters (I know now that they don't exist ;-)
|
||||
// commandsToCheck.add("E" + first + second + third);
|
||||
}
|
||||
commandsToCheck.add("E" + first + second);
|
||||
}
|
||||
}
|
||||
|
||||
// if you only want to try specific commands, perhaps because they failed in the big run, comment out the above
|
||||
// and use this instead
|
||||
// commandsToCheck.addAll(Arrays.asList("RH1", "RH2", "RH3", "TP1", "TP2", "TP3", "UL1", "UL2", "UL3", "UMX",
|
||||
// "UM1", "UM2", "UM3", "UPD", "TCP"));
|
||||
|
||||
while (!commandsToCheck.isEmpty()) {
|
||||
if (commandsToCheck.size() % 100 == 0) {
|
||||
LOGGER.debug(commandsToCheck.size() + " left to check");
|
||||
}
|
||||
try {
|
||||
if (checkIsValidCommand(commandsToCheck.get(0))) {
|
||||
validCommands.add(commandsToCheck.get(0));
|
||||
commandsToCheck.remove(0);
|
||||
} else {
|
||||
commandsToCheck.remove(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Sleeping after Exception: " + e.getLocalizedMessage());
|
||||
|
||||
if (lastFailedCommand.equals(commandsToCheck.get(0))) {
|
||||
failedCommandRetry = failedCommandRetry + 1;
|
||||
if (failedCommandRetry >= 5) {
|
||||
failedCommands.add(commandsToCheck.get(0));
|
||||
commandsToCheck.remove(0);
|
||||
}
|
||||
} else {
|
||||
failedCommandRetry = 0;
|
||||
lastFailedCommand = commandsToCheck.get(0);
|
||||
}
|
||||
try {
|
||||
// Backoff somewhat nicely
|
||||
Thread.sleep(2 * failedCommandRetry * failedCommandRetry * failedCommandRetry);
|
||||
} catch (InterruptedException e1) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
} catch (InterruptedException e1) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("\nValid commands:");
|
||||
|
||||
for (String validCommand : validCommands) {
|
||||
LOGGER.info(validCommand);
|
||||
}
|
||||
|
||||
LOGGER.info("\nFailed commands:");
|
||||
|
||||
for (String failedCommand : failedCommands) {
|
||||
LOGGER.info(failedCommand + "\", \"");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkIsValidCommand(String command)
|
||||
throws InterruptedException, UnknownHostException, SolarMaxException {
|
||||
List<String> commands = new ArrayList<String>();
|
||||
commands.add(command);
|
||||
|
||||
Map<String, @Nullable String> responseMap = null;
|
||||
|
||||
responseMap = getValuesFromSolarMax(HOST, PORT, commands);
|
||||
|
||||
if (responseMap.containsKey(command)) {
|
||||
LOGGER.debug("Request: " + command + " Valid Response: " + responseMap.get(command));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* based on SolarMaxConnector.getValuesFromSolarMax
|
||||
*/
|
||||
private static Map<String, @Nullable String> getValuesFromSolarMax(final String host, int port,
|
||||
final List<String> commandList) throws SolarMaxException {
|
||||
Socket socket;
|
||||
|
||||
Map<String, @Nullable String> 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, CONNECTION_TIMEOUT);
|
||||
|
||||
int firstCommandNumber = requestNumber * maxConcurrentCommands;
|
||||
int lastCommandNumber = (requestNumber + 1) * maxConcurrentCommands;
|
||||
if (lastCommandNumber > commandList.size()) {
|
||||
lastCommandNumber = commandList.size();
|
||||
}
|
||||
List<String> 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 Map<String, @Nullable String> getValuesFromSolarMax(final Socket socket,
|
||||
final List<String> 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 List<String> getCharacters() {
|
||||
List<String> characters = new ArrayList<>();
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
characters.add(Character.toString(c));
|
||||
}
|
||||
for (char c = 'A'; c <= 'Z'; c++) {
|
||||
characters.add(Character.toString(c));
|
||||
}
|
||||
characters.add("0");
|
||||
characters.add("1");
|
||||
characters.add("2");
|
||||
characters.add("3");
|
||||
characters.add("4");
|
||||
characters.add("5");
|
||||
characters.add("6");
|
||||
characters.add("7");
|
||||
characters.add("8");
|
||||
characters.add("9");
|
||||
|
||||
characters.add(".");
|
||||
characters.add("-");
|
||||
characters.add("_");
|
||||
|
||||
return characters;
|
||||
}
|
||||
|
||||
private static Socket getSocketConnection(final String host, int port)
|
||||
throws SolarMaxConnectionException, UnknownHostException {
|
||||
port = (port == 0) ? SolarmaxConnectorFindCommands.PORT : port;
|
||||
|
||||
Socket socket;
|
||||
|
||||
try {
|
||||
socket = new Socket();
|
||||
LOGGER.debug(" Connecting to " + host + ":" + port + " with a timeout of " + CONNECTION_TIMEOUT);
|
||||
socket.connect(new InetSocketAddress(host, port), CONNECTION_TIMEOUT);
|
||||
LOGGER.debug(" Connected.");
|
||||
socket.setSoTimeout(CONNECTION_TIMEOUT);
|
||||
} catch (final UnknownHostException e) {
|
||||
throw e;
|
||||
} catch (final IOException e) {
|
||||
throw new SolarMaxConnectionException("Error connecting to port '" + port + "' on host '" + host + "'", e);
|
||||
}
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private static Map<String, @Nullable String> getValuesFromSolarMax(final OutputStream outputStream,
|
||||
final InputStream inputStream, final List<String> commandList) throws SolarMaxException {
|
||||
Map<String, @Nullable String> returnedValues;
|
||||
String commandString = getCommandString(commandList);
|
||||
String request = SolarMaxConnector.contructRequest(commandString);
|
||||
try {
|
||||
LOGGER.trace(" ==>: {}", request);
|
||||
|
||||
outputStream.write(request.getBytes());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static String getCommandString(List<String> commandList) {
|
||||
String commandString = "";
|
||||
for (String command : commandList) {
|
||||
if (!commandString.isEmpty()) {
|
||||
commandString = commandString + ";";
|
||||
}
|
||||
commandString = commandString + command;
|
||||
}
|
||||
return commandString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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<String, @Nullable String> extractValuesFromResponse(String response) {
|
||||
final Map<String, @Nullable String> 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 responseKey = entry.substring(0, 3);
|
||||
|
||||
String responseValue = (entry.length() >= 5) ? entry.substring(4) : null;
|
||||
|
||||
responseMap.put(responseKey, responseValue);
|
||||
}
|
||||
}
|
||||
|
||||
return responseMap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user