From: Hilbrand Bouwkamp Date: Tue, 12 Jan 2021 21:15:21 +0000 (+0100) Subject: [kostalinverter] Fixed package structure (#9559) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=e3ae01df9102a378bdaff43062d5b491617077e3;p=openhab-addons.git [kostalinverter] Fixed package structure (#9559) The internal package was placed before the binding specific package, and it should be the other way around. Signed-off-by: Hilbrand Bouwkamp --- diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/KostalInverterFactory.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/KostalInverterFactory.java deleted file mode 100644 index 47a3c4926e..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/KostalInverterFactory.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * 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.internal.kostal.inverter; - -import static org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationBindingConstants.*; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.internal.kostal.inverter.firstgeneration.WebscrapeHandler; -import org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationHandler; -import org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationInverterTypes; -import org.openhab.core.io.net.http.HttpClientFactory; -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.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -/** - * @author Christian Schneider - Initial contribution (as WebscrapeHandlerFactory.java) - * @author René Stakemeier - extension for the third generation of KOSTAL inverters - */ -@Component(service = ThingHandlerFactory.class, configurationPid = "binding.kostalinverter") -@NonNullByDefault -public class KostalInverterFactory extends BaseThingHandlerFactory { - - private static final Map SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS = new HashMap<>(); - static { - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ42, ThirdGenerationInverterTypes.PIKOIQ_42); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ55, ThirdGenerationInverterTypes.PIKOIQ_55); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ70, ThirdGenerationInverterTypes.PIKOIQ_70); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ85, ThirdGenerationInverterTypes.PIKOIQ_85); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ100, ThirdGenerationInverterTypes.PIKOIQ_100); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS42WITHBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITH_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS55WITHBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITH_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS70WITHBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITH_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS85WITHBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITH_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS100WITHBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITH_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS42WITHOUTBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITHOUT_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS55WITHOUTBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITHOUT_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS70WITHOUTBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITHOUT_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS85WITHOUTBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITHOUT_BATTERY); - SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS100WITHOUTBATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITHOUT_BATTERY); - } - - public static final ThingTypeUID FIRST_GENERATION_INVERTER = new ThingTypeUID("kostalinverter", "kostalinverter"); - - private final HttpClient httpClient; - - @Activate - public KostalInverterFactory(@Reference final HttpClientFactory httpClientFactory) { - this.httpClient = httpClientFactory.getCommonHttpClient(); - } - - @Override - public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return thingTypeUID.equals(FIRST_GENERATION_INVERTER) - || SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.keySet().contains(thingTypeUID); - } - - @Override - protected @Nullable ThingHandler createHandler(Thing thing) { - // first generation - if (FIRST_GENERATION_INVERTER.equals(thing.getThingTypeUID())) { - return new WebscrapeHandler(thing); - } - // third generation - ThirdGenerationInverterTypes inverterType = SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS - .get(thing.getThingTypeUID()); - if (inverterType != null) { - return new ThirdGenerationHandler(thing, httpClient, inverterType); - } - return null; - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/ChannelConfig.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/ChannelConfig.java deleted file mode 100644 index 145c53292a..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/ChannelConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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.internal.kostal.inverter.firstgeneration; - -import javax.measure.Unit; - -/** - * @author Christian Schneider - Initial contribution - * @author Christoph Weitkamp - Incorporated new QuantityType (Units of Measurement) - */ -public class ChannelConfig { - public ChannelConfig(String id, String tag, int num, Unit unit) { - this.id = id; - this.tag = tag; - this.num = num; - this.unit = unit; - } - - String id; - String tag; - int num; - Unit unit; -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/SourceConfig.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/SourceConfig.java deleted file mode 100644 index 04c7060b1e..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/SourceConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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.internal.kostal.inverter.firstgeneration; - -/** - * @author Christian Schneider - Initial contribution - */ -public class SourceConfig { - public String url; - public String userName; - public String password; - public int refreshInterval; -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/WebscrapeHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/WebscrapeHandler.java deleted file mode 100644 index 5238893aa3..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/firstgeneration/WebscrapeHandler.java +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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.internal.kostal.inverter.firstgeneration; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.measure.Unit; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.types.StringType; -import org.openhab.core.library.unit.Units; -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.openhab.core.types.UnDefType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Christian Schneider - Initial contribution - * @author Christoph Weitkamp - Incorporated new QuantityType (Units of Measurement) - */ -public class WebscrapeHandler extends BaseThingHandler { - private Logger logger = LoggerFactory.getLogger(WebscrapeHandler.class); - private SourceConfig config; - - private final List channelConfigs = new ArrayList<>(); - - public WebscrapeHandler(Thing thing) { - super(thing); - channelConfigs.add(new ChannelConfig("acPower", "td", 4, Units.WATT)); - channelConfigs.add(new ChannelConfig("totalEnergy", "td", 7, Units.KILOWATT_HOUR)); - channelConfigs.add(new ChannelConfig("dayEnergy", "td", 10, Units.KILOWATT_HOUR)); - channelConfigs.add(new ChannelConfig("status", "td", 13, null)); - channelConfigs.add(new ChannelConfig("str1Voltage", "td", 19, Units.VOLT)); - channelConfigs.add(new ChannelConfig("str1Current", "td", 25, Units.AMPERE)); - channelConfigs.add(new ChannelConfig("str2Voltage", "td", 33, Units.VOLT)); - channelConfigs.add(new ChannelConfig("str2Current", "td", 39, Units.AMPERE)); - channelConfigs.add(new ChannelConfig("l1Voltage", "td", 22, Units.VOLT)); - channelConfigs.add(new ChannelConfig("l1Power", "td", 28, Units.WATT)); - channelConfigs.add(new ChannelConfig("l2Voltage", "td", 36, Units.VOLT)); - channelConfigs.add(new ChannelConfig("l2Power", "td", 42, Units.WATT)); - channelConfigs.add(new ChannelConfig("l3Voltage", "td", 46, Units.VOLT)); - channelConfigs.add(new ChannelConfig("l3Power", "td", 49, Units.WATT)); - } - - @Override - public void initialize() { - config = getConfigAs(SourceConfig.class); - scheduler.scheduleWithFixedDelay(() -> { - try { - refresh(); - updateStatus(ThingStatus.ONLINE); - } catch (Exception e) { - logger.debug("Error refreshing source '{}'", getThing().getUID(), e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - e.getClass().getName() + ":" + e.getMessage()); - } - }, 0, config.refreshInterval, TimeUnit.SECONDS); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // Read only - } - - private void refresh() throws Exception { - Document doc = getDoc(); - for (ChannelConfig cConfig : channelConfigs) { - Channel channel = getThing().getChannel(cConfig.id); - if (channel != null) { - String value = getTag(doc, cConfig.tag).get(cConfig.num); - updateState(channel.getUID(), getState(value, cConfig.unit)); - } - } - } - - private static List getTag(Document doc, String tag) { - List result = new ArrayList<>(); - Iterator elIt = doc.getElementsByTag(tag).iterator(); - while (elIt.hasNext()) { - String content = elIt.next().text(); - content = content.replace("\u00A0", "").trim(); - if (!content.isEmpty()) { - result.add(content); - } - } - return result; - } - - private Document getDoc() throws IOException { - String login = config.userName + ":" + config.password; - String base64login = new String(Base64.getEncoder().encode(login.getBytes())); - return Jsoup.connect(config.url).header("Authorization", "Basic " + base64login).get(); - } - - private State getState(String value, Unit unit) { - if (unit == null) { - return new StringType(value); - } else { - try { - return new QuantityType<>(new BigDecimal(value), unit); - } catch (NumberFormatException e) { - logger.debug("Error parsing value '{}'", value, e); - return UnDefType.UNDEF; - } - } - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationBindingConstants.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationBindingConstants.java deleted file mode 100644 index 9390ccb0e9..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationBindingConstants.java +++ /dev/null @@ -1,144 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.thing.ThingTypeUID; - -/** - * The {@link ThirdGenerationBindingConstants} class defines common constants, which are - * used across the whole binding. - * - * @author René Stakemeier - Initial contribution - */ -@NonNullByDefault -public class ThirdGenerationBindingConstants { - - private static final String BINDING_ID = "kostalinverter"; - - // List of all constants used for the authentication - static final String USER_TYPE = "user"; - static final String HMAC_SHA256_ALGORITHM = "HMACSHA256"; - static final String SHA_256_HASH = "SHA-256"; - static final int AES_GCM_TAG_LENGTH = 128; // bit count - - // List of all Thing Type UIDs - public static final ThingTypeUID PIKOIQ42 = new ThingTypeUID(BINDING_ID, "PIKOIQ42"); - public static final ThingTypeUID PIKOIQ55 = new ThingTypeUID(BINDING_ID, "PIKOIQ55"); - public static final ThingTypeUID PIKOIQ70 = new ThingTypeUID(BINDING_ID, "PIKOIQ70"); - public static final ThingTypeUID PIKOIQ85 = new ThingTypeUID(BINDING_ID, "PIKOIQ85"); - public static final ThingTypeUID PIKOIQ100 = new ThingTypeUID(BINDING_ID, "PIKOIQ100"); - public static final ThingTypeUID PLENTICOREPLUS42WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS42WITHOUTBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS55WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS55WITHOUTBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS70WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS70WITHOUTBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS85WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS85WITHOUTBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS100WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS100WITHOUTBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS42WITHBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS42WITHBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS55WITHBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS55WITHBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS70WITHBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS70WITHBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS85WITHBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS85WITHBATTERY"); - public static final ThingTypeUID PLENTICOREPLUS100WITHBATTERY = new ThingTypeUID(BINDING_ID, - "PLENTICOREPLUS100WITHBATTERY"); - - // List of error messages - public static final String COMMUNICATION_ERROR_AUTHENTICATION = "Error during the initialisation of the authentication"; - public static final String COMMUNICATION_ERROR_UNSUPPORTED_ENCODING = "The text encoding is not supported by this openHAB installation"; - public static final String COMMUNICATION_ERROR_AES_ERROR = "The java installation does not support AES encryption in GCM mode"; - public static final String COMMUNICATION_ERROR_HTTP = "HTTP communication error: No response from device"; - public static final String COMMUNICATION_ERROR_JSON = "HTTP communication error: answer did not match the expected format"; - public static final String COMMUNICATION_ERROR_API_CHANGED = "The API seems to have changed :-( Maybe this implementation has become incompatible with the device"; - public static final String COMMUNICATION_ERROR_INCOMPATIBLE_DEVICE = "The device could not provide the required information. Please check, if you selected the right thing for your device!"; - public static final String COMMUNICATION_ERROR_USER_ACCOUNT_LOCKED = "Your user account on the device is locked. Please reset the password by following the instructions on the device´s web frontend"; - public static final String CONFIGURATION_ERROR_PASSWORD = "Wrong password"; - - // List of all Channel uids - public static final String CHANNEL_DEVICE_LOCAL_DC_POWER = "deviceLocalDCPower"; - public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_BATTERY = "deviceLocalHomeconsumptionFromBattery"; - public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_GRID = "deviceLocalHomeconsumptionFromGrid"; - public static final String CHANNEL_DEVICE_LOCAL_OWNCONSUMPTION = "deviceLocalOwnconsumption"; - public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_PV = "deviceLocalHomeconsumptionFromPV"; - public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_TOTAL = "deviceLocalHomeconsumptionTotal"; - public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_ABSOLUTE = "deviceLocalLimitEVUAbsolute"; - public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_RELATIV = "deviceLocalLimitEVURelativ"; - public static final String CHANNEL_DEVICE_LOCAL_WORKTIME = "deviceLocalWorktime"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE = "deviceLocalACPhase1CurrentAmperage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER = "deviceLocalACPhase1CurrentPower"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_VOLTAGE = "deviceLocalACPhase1CurrentVoltage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_AMPERAGE = "deviceLocalACPhase2CurrentAmperage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_POWER = "deviceLocalACPhase2CurrentPower"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_VOLTAGE = "deviceLocalACPhase2CurrentVoltage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_AMPERAGE = "deviceLocalACPhase3CurrentAmperage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_POWER = "deviceLocalACPhase3CurrentPower"; - public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_VOLTAGE = "deviceLocalACPhase3CurrentVoltage"; - public static final String CHANNEL_DEVICE_LOCAL_AC_CURRENT_POWER = "deviceLocalACCurrentPower"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_LOADING_CYCLES = "deviceLocalBatteryLoadingCycles"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_FULL_CHARGE_CAPACITY = "deviceLocalBatteryFullChargeCapacity"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_AMPERAGE = "deviceLocalBatteryAmperage"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_POWER = "deviceLocalBatteryPower"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_STATE_OF_CHARGE = "deviceLocalBatteryStageOfCharge"; - public static final String CHANNEL_DEVICE_LOCAL_BATTERY_VOLTAGE = "deviceLocalBatteryVoltage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_AMPERAGE = "deviceLocalPVString1Amperage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_POWER = "deviceLocalPVString1Power"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_VOLTAGE = "deviceLocalPVString1Voltage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_AMPERAGE = "deviceLocalPVString2Amperage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_POWER = "deviceLocalPVString2Power"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_VOLTAGE = "deviceLocalPVString2Voltage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_AMPERAGE = "deviceLocalPVString3Amperage"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_POWER = "deviceLocalPVString3Power"; - public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_VOLTAGE = "deviceLocalPVString3Voltage"; - public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_MC = "SCBEventErrorCountMc"; - public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_SFH = "SCBEventErrorCountSFH"; - public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_SCB = "SCBEventErrorCountSCB"; - public static final String CHANNEL_SCB_EVENT_WARNING_COUNT_SCB = "SCBEventWarningCountSCB"; - public static final String CHANNEL_STATISTIC_AUTARKY_DAY = "statisticAutarkyDay"; - public static final String CHANNEL_STATISTIC_AUTARKY_MONTH = "statisticAutarkyMonth"; - public static final String CHANNEL_STATISTIC_AUTARKY_TOTAL = "statisticAutarkyTotal"; - public static final String CHANNEL_STATISTIC_AUTARKY_YEAR = "statisticAutarkyYear"; - public static final String CHANNEL_STATISTIC_CO2SAVING_DAY = "statisticCo2SavingDay"; - public static final String CHANNEL_STATISTIC_CO2SAVING_MONTH = "statisticCo2SavingMonth"; - public static final String CHANNEL_STATISTIC_CO2SAVING_TOTAL = "statisticCo2SavingTotal"; - public static final String CHANNEL_STATISTIC_CO2SAVING_YEAR = "statisticCo2SavingYear"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_DAY = "statisticHomeconsumptionDay"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_MONTH = "statisticHomeconsumptionMonth"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_TOTAL = "statisticHomeconsumptionTotal"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_YEAR = "statisticHomeconsumptionYear"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_DAY = "statisticHomeconsumptionFromBatteryDay"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_MONTH = "statisticHomeconsumptionFromBatteryMonth"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_TOTAL = "statisticHomeconsumptionFromBatteryTotal"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_YEAR = "statisticHomeconsumptionFromBatteryYear"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_DAY = "statisticHomeconsumptionFromGridDay"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_MONTH = "statisticHomeconsumptionFromGridMonth"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_TOTAL = "statisticHomeconsumptionFromGridTotal"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_YEAR = "statisticHomeconsumptionFromGridYear"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_DAY = "statisticHomeconsumptionFromPVDay"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_MONTH = "statisticHomeconsumptionFromPVMonth"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_TOTAL = "statisticHomeconsumptionFromPVTotal"; - public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_YEAR = "statisticHomeconsumptionFromPVYear"; - public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_DAY = "statisticOwnconsumptionRateDay"; - public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_MONTH = "statisticOwnconsumptionRateMonth"; - public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_TOTAL = "statisticOwnconsumptionRateTotal"; - public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_YEAR = "statisticOwnconsumptionRateYear"; - public static final String CHANNEL_STATISTIC_YIELD_DAY = "statisticYieldDay"; - public static final String CHANNEL_STATISTIC_YIELD_MONTH = "statisticYieldMonth"; - public static final String CHANNEL_STATISTIC_YIELD_TOTAL = "statisticYieldTotal"; - public static final String CHANNEL_STATISTIC_YIELD_YEAR = "statisticYieldYear"; -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelDatatypes.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelDatatypes.java deleted file mode 100644 index 1004ee2a1a..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelDatatypes.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -/** - * The {@link ThirdGenerationChannelDatatypes} enumeration contains the data types provided by the device - * - * @author René Stakemeier - Initial contribution - */ -enum ThirdGenerationChannelDatatypes { - INTEGER, - PERCEMTAGE, - KILOGRAM, - SECONDS, - KILOWATT_HOUR, - WATT, - AMPERE, - AMPERE_HOUR, - VOLT -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java deleted file mode 100644 index ebb299b814..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -/*** - * The {@link ThirdGenerationChannelMappingToWebApi}} is used to map the channel name to the web API commands - * - * @author René Stakemeier - Initial contribution - * - */ -class ThirdGenerationChannelMappingToWebApi { - - String channelUID; - String moduleId; - String processdataId; - ThirdGenerationChannelDatatypes dataType; - - /** - * Constructor of {@link ThirdGenerationChannelMappingToWebApi} - * - * @param channelUID The channel UUID - * @param moduleId module id (as defined by the web api) - * @param processdataId process data id (as defined by the web api) - * @param dataType data type of this channel - */ - ThirdGenerationChannelMappingToWebApi(String channelUID, String moduleId, String processdataId, - ThirdGenerationChannelDatatypes dataType) { - this.channelUID = channelUID; - this.moduleId = moduleId; - this.processdataId = processdataId; - this.dataType = dataType; - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationConfiguration.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationConfiguration.java deleted file mode 100644 index a77a6a2cf7..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -/** - * The {@link ThirdGenerationConfiguration} class contains fields mapping thing configuration parameters. - * - * @author René Stakemeier - Initial contribution - */ -public class ThirdGenerationConfiguration { - - public String url; - public String userPassword; - public int refreshInternalInSeconds; -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationEncryptionHelper.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationEncryptionHelper.java deleted file mode 100644 index db4079d167..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationEncryptionHelper.java +++ /dev/null @@ -1,121 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -import static org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationBindingConstants.*; - -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.Base64; -import java.util.Random; - -import javax.crypto.Mac; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -/** - * The {@link ThirdGenerationEncryptionHelper} is responsible for handling the encryption for the authentication - * handlers. - * - * @author René Stakemeier - Initial contribution - */ -final class ThirdGenerationEncryptionHelper { - - private ThirdGenerationEncryptionHelper() { - } - - /** - * This method generates the HMACSha256 encrypted value of the given value - * - * @param password Password used for encryption - * @param valueToEncrypt value to encrypt - * @return encrypted value - * @throws InvalidKeyException thrown if the key generated from the password is invalid - * @throws NoSuchAlgorithmException thrown if HMAC SHA 256 is not supported - */ - static byte[] getHMACSha256(byte[] password, String valueToEncrypt) - throws InvalidKeyException, NoSuchAlgorithmException { - SecretKeySpec signingKey = new SecretKeySpec(password, HMAC_SHA256_ALGORITHM); - Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); - mac.init(signingKey); - mac.update(valueToEncrypt.getBytes()); - return mac.doFinal(); - } - - /** - * This methods generates the client proof. - * It is calculated as XOR between the {@link clientSignature} and the {@link serverSignature} - * - * @param clientSignature client signature - * @param serverSignature server signature - * @return client proof - */ - static String createClientProof(byte[] clientSignature, byte[] serverSignature) { - byte[] result = new byte[clientSignature.length]; - for (int i = 0; i < clientSignature.length; i++) { - result[i] = (byte) (0xff & (clientSignature[i] ^ serverSignature[i])); - } - return Base64.getEncoder().encodeToString(result); - } - - /** - * Create the PBKDF2 hash - * - * @param password password - * @param salt salt - * @param rounds rounds - * @return hash - * @throws NoSuchAlgorithmException if PBKDF2WithHmacSHA256 is not supported - * @throws InvalidKeySpecException if the key specification is not supported - */ - static byte[] getPBKDF2Hash(String password, byte[] salt, int rounds) - throws NoSuchAlgorithmException, InvalidKeySpecException { - PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, rounds, 256); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - return skf.generateSecret(spec).getEncoded(); - } - - /** - * Create the SHA256 hash value for the given byte array - * - * @param valueToHash byte array to get the hash value for - * @return the hash value - * @throws NoSuchAlgorithmException if SHA256 is not supported - */ - static byte[] getSha256Hash(byte[] valueToHash) throws NoSuchAlgorithmException { - return MessageDigest.getInstance(SHA_256_HASH).digest(valueToHash); - } - - /** - * Create the nonce (numbers used once) for the client for communication - * - * @return nonce - */ - static String createClientNonce() { - Random generator = new Random(); - - // Randomize the random generator - byte[] randomizeArray = new byte[1024]; - generator.nextBytes(randomizeArray); - - // 3 words of 4 bytes are required for the handshake - byte[] nonceArray = new byte[12]; - generator.nextBytes(nonceArray); - - // return the base64 encoded value of the random words - return Base64.getMimeEncoder().encodeToString(nonceArray); - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHandler.java deleted file mode 100644 index 7f815b7171..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHandler.java +++ /dev/null @@ -1,485 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -import static org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationBindingConstants.*; - -import java.io.UnsupportedEncodingException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.Mac; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.library.unit.SIUnits; -import org.openhab.core.library.unit.Units; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; - -/** - * The {@link ThirdGenerationHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author René Stakemeier - Initial contribution - */ -@NonNullByDefault -public class ThirdGenerationHandler extends BaseThingHandler { - - /* - * operations used for authentication - */ - private static final String AUTH_START = "/auth/start"; - private static final String AUTH_FINISH = "/auth/finish"; - private static final String AUTH_CREATE_SESSION = "/auth/create_session"; - - /* - * operations used for gathering process data from the device - */ - private static final String PROCESSDATA = "/processdata"; - - /* - * After the authentication the result (the session id) is stored here and used to "sign" future requests - */ - private @Nullable String sessionId; - /* - * The configuration file containing the host, the password and the refresh interval - */ - private @NonNullByDefault({}) ThirdGenerationConfiguration config; - - private @Nullable ScheduledFuture refreshScheduler; - - private @Nullable HttpClient httpClient; - - private ThirdGenerationInverterTypes inverterType; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - /** - * Constructor of this class - * - * @param thing the thing - * @param httpClient the httpClient used for communication - * @param inverterType the type of the device - */ - public ThirdGenerationHandler(Thing thing, HttpClient httpClient, ThirdGenerationInverterTypes inverterType) { - super(thing); - this.inverterType = inverterType; - this.httpClient = httpClient; - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // All channels are readonly and updated by the scheduler - } - - @Override - public void dispose() { - if (refreshScheduler != null) { - refreshScheduler.cancel(true); - refreshScheduler = null; - } - super.dispose(); - } - - @Override - public void initialize() { - config = getConfigAs(ThirdGenerationConfiguration.class); - // temporary value while initializing - updateStatus(ThingStatus.UNKNOWN); - - // Start the authentication - scheduler.schedule(this::authenticate, 1, TimeUnit.SECONDS); - - // Start the update scheduler as configured - refreshScheduler = scheduler.scheduleWithFixedDelay(this::updateChannelValues, 10, - config.refreshInternalInSeconds, TimeUnit.SECONDS); - } - - /** - * The API supports the resolution of multiple values at a time - * - * Therefore this methods builds one request to gather all information for the current inverter. - * The list contains all channels as defined in {@link ThirdGenerationMappingInverterToChannel} for the - * current inverter - * - */ - private void updateChannelValues() { - Map> channelList = ThirdGenerationMappingInverterToChannel - .getModuleToChannelsMappingForInverter(inverterType); - JsonArray updateMessageJsonArray = getUpdateChannelMessage(channelList); - - // Send the API request to get values for all channels - ContentResponse updateMessageContentResponse; - try { - updateMessageContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, - PROCESSDATA, updateMessageJsonArray, sessionId); - if (updateMessageContentResponse.getStatus() == 404) { - // Module not found - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_INCOMPATIBLE_DEVICE); - return; - } - if (updateMessageContentResponse.getStatus() == 503) { - // Communication error (e.g. during initial boot of the SCB) - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_HTTP); - return; - } - if (updateMessageContentResponse.getStatus() == 401) { - // session not valid (timed out? device rebooted?) - logger.info("Session expired - performing retry"); - authenticate(); - // Retry - updateMessageContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, - PROCESSDATA, updateMessageJsonArray, sessionId); - } - } catch (TimeoutException | ExecutionException e) { - // Communication problem - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); - return; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); - return; - } - JsonArray updateMessageResultsJsonArray = ThirdGenerationHttpHelper - .getJsonArrayFromResponse(updateMessageContentResponse); - - // Map the returned values back to the channels and update them - for (int i = 0; i < updateMessageResultsJsonArray.size(); i++) { - JsonObject moduleAnswer = updateMessageResultsJsonArray.get(i).getAsJsonObject(); - String moduleName = moduleAnswer.get("moduleid").getAsString(); - JsonArray processdata = moduleAnswer.get("processdata").getAsJsonArray(); - for (int j = 0; j < processdata.size(); j++) { - // Update the channels with their new value - JsonObject newValueObject = processdata.get(j).getAsJsonObject(); - String valueId = newValueObject.get("id").getAsString(); - double valueAsDouble = newValueObject.get("value").getAsDouble(); - ThirdGenerationChannelMappingToWebApi channel = channelList.get(moduleName).stream() - .filter(c -> c.moduleId.equals(moduleName) && c.processdataId.equals(valueId)).findFirst() - .get(); - updateChannelValue(channel.channelUID, channel.dataType, valueAsDouble); - } - } - updateStatus(ThingStatus.ONLINE); - } - - /** - * Update the channel to the given value. - * The value is set to the matching data (SITypes etc) - * - * @param channeluid Channel to update - * @param dataType target data type - * @param value value - */ - private void updateChannelValue(String channeluid, ThirdGenerationChannelDatatypes dataType, Double value) { - switch (dataType) { - case INTEGER: { - updateState(channeluid, new DecimalType(value.longValue())); - break; - } - case PERCEMTAGE: { - updateState(channeluid, new QuantityType<>(value, Units.PERCENT)); - break; - } - case KILOGRAM: { - updateState(channeluid, new QuantityType<>(value / 1000, SIUnits.KILOGRAM)); - break; - } - case SECONDS: { - updateState(channeluid, new QuantityType<>(value, Units.SECOND)); - break; - } - case KILOWATT_HOUR: { - updateState(channeluid, new QuantityType<>(value / 1000, Units.KILOWATT_HOUR)); - break; - } - case WATT: { - updateState(channeluid, new QuantityType<>(value, Units.WATT)); - break; - } - case AMPERE: { - updateState(channeluid, new QuantityType<>(value, Units.AMPERE)); - break; - } - case AMPERE_HOUR: { - // Ampere hours is not a supported unit, but 1 AH is equal tp 3600 coulomb... - updateState(channeluid, new QuantityType<>(value * 3600, Units.COULOMB)); - break; - } - case VOLT: { - updateState(channeluid, new QuantityType<>(value, Units.VOLT)); - break; - } - default: { - // unknown datatype - logger.debug("{} not known!", dataType); - } - } - } - - /** - * Creates the message which has to be send to the inverter to gather the current informations for all channels - * - * @param channelList channels of this thing - * @return the JSON array to send to the device - */ - private JsonArray getUpdateChannelMessage(Map> channelList) { - // Build the message to send to the inverter - JsonArray updateMessageJsonArray = new JsonArray(); - for (Entry> moduleId : channelList.entrySet()) { - JsonObject moduleJsonObject = new JsonObject(); - moduleJsonObject.addProperty("moduleid", moduleId.getKey()); - - JsonArray processdataNames = new JsonArray(); - for (ThirdGenerationChannelMappingToWebApi processdata : channelList.get(moduleId.getKey())) { - processdataNames.add(processdata.processdataId); - } - moduleJsonObject.add("processdataids", processdataNames); - updateMessageJsonArray.add(moduleJsonObject); - } - return updateMessageJsonArray; - } - - /** - * This function is used to authenticate against the SCB. - * SCB uses PBKDF2 and AES256 GCM mode with a slightly modified authentication message. - * The authentication will fail on JRE < 8u162. since the security policy is set to "limited" by default (see readme - * for fix) - */ - private final void authenticate() { - // Create random numbers - String clientNonce = ThirdGenerationEncryptionHelper.createClientNonce(); - // Perform first step of authentication - JsonObject authMeJsonObject = new JsonObject(); - authMeJsonObject.addProperty("username", USER_TYPE); - authMeJsonObject.addProperty("nonce", clientNonce); - - ContentResponse authStartResponseContentResponse; - try { - authStartResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, - AUTH_START, authMeJsonObject); - - // 200 is the desired status code - int statusCode = authStartResponseContentResponse.getStatus(); - - if (statusCode == 400) { - // Invalid user (which is hard coded and therefore can not be wrong until the api is changed by the - // manufacturer - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_API_CHANGED); - return; - } - if (statusCode == 403) { - // User is logged - // This can happen, if the user had to many bad attempts of entering the password in the web - // front end - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - COMMUNICATION_ERROR_USER_ACCOUNT_LOCKED); - return; - } - - if (statusCode == 503) { - // internal communication error - // This can happen if the device is not ready yet for communication - updateStatus(ThingStatus.UNINITIALIZED); - return; - } - } catch (InterruptedException | TimeoutException | ExecutionException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); - return; - } - JsonObject authMeResponseJsonObject = ThirdGenerationHttpHelper - .getJsonObjectFromResponse(authStartResponseContentResponse); - - // Extract information from the response - String salt = authMeResponseJsonObject.get("salt").getAsString(); - String serverNonce = authMeResponseJsonObject.get("nonce").getAsString(); - int rounds = authMeResponseJsonObject.get("rounds").getAsInt(); - String transactionId = authMeResponseJsonObject.get("transactionId").getAsString(); - - // Do the cryptography stuff (magic happens here) - byte[] saltedPasswort; - byte[] clientKey; - byte[] serverKey; - byte[] storedKey; - byte[] clientSignature; - byte[] serverSignature; - String authMessage; - try { - saltedPasswort = ThirdGenerationEncryptionHelper.getPBKDF2Hash(config.userPassword, - Base64.getDecoder().decode(salt), rounds); - clientKey = ThirdGenerationEncryptionHelper.getHMACSha256(saltedPasswort, "Client Key"); - serverKey = ThirdGenerationEncryptionHelper.getHMACSha256(saltedPasswort, "Server Key"); - storedKey = ThirdGenerationEncryptionHelper.getSha256Hash(clientKey); - authMessage = String.format("n=%s,r=%s,r=%s,s=%s,i=%d,c=biws,r=%s", USER_TYPE, clientNonce, serverNonce, - salt, rounds, serverNonce); - clientSignature = ThirdGenerationEncryptionHelper.getHMACSha256(storedKey, authMessage); - serverSignature = ThirdGenerationEncryptionHelper.getHMACSha256(serverKey, authMessage); - } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | IllegalStateException e2) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_AUTHENTICATION); - return; - } - String clientProof = ThirdGenerationEncryptionHelper.createClientProof(clientSignature, clientKey); - // Perform step 2 of the authentication - JsonObject authFinishJsonObject = new JsonObject(); - authFinishJsonObject.addProperty("transactionId", transactionId); - authFinishJsonObject.addProperty("proof", clientProof); - - ContentResponse authFinishResponseContentResponse; - JsonObject authFinishResponseJsonObject; - try { - authFinishResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, - AUTH_FINISH, authFinishJsonObject); - authFinishResponseJsonObject = ThirdGenerationHttpHelper - .getJsonObjectFromResponse(authFinishResponseContentResponse); - // 200 is the desired status code - if (authFinishResponseContentResponse.getStatus() == 400) { - // Authentication failed - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - CONFIGURATION_ERROR_PASSWORD); - return; - } - } catch (InterruptedException | TimeoutException | ExecutionException e3) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); - return; - } - - // Extract information from the response - byte[] signature = Base64.getDecoder().decode(authFinishResponseJsonObject.get("signature").getAsString()); - String token = authFinishResponseJsonObject.get("token").getAsString(); - - // Validate provided signature against calculated signature - if (!java.util.Arrays.equals(serverSignature, signature)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_AUTHENTICATION); - return; - } - - // Calculate protocol key - SecretKeySpec signingKey = new SecretKeySpec(storedKey, HMAC_SHA256_ALGORITHM); - Mac mac; - byte[] protocolKeyHMAC; - try { - mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); - mac.init(signingKey); - mac.update("Session Key".getBytes()); - mac.update(authMessage.getBytes()); - mac.update(clientKey); - protocolKeyHMAC = mac.doFinal(); - } catch (NoSuchAlgorithmException | InvalidKeyException e1) { - // Since the necessary libraries are provided, this should not happen - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - COMMUNICATION_ERROR_AUTHENTICATION); - return; - } - - byte[] data; - byte[] iv; - - // AES GCM stuff - iv = new byte[16]; - - new SecureRandom().nextBytes(iv); - - SecretKeySpec skeySpec = new SecretKeySpec(protocolKeyHMAC, "AES"); - GCMParameterSpec param = new GCMParameterSpec(protocolKeyHMAC.length * 8 - AES_GCM_TAG_LENGTH, iv); - - Cipher cipher; - try { - cipher = Cipher.getInstance("AES_256/GCM/NOPADDING"); - cipher.init(Cipher.ENCRYPT_MODE, skeySpec, param); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException e1) { - // The java installation does not support AES encryption in GCM mode - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_AUTHENTICATION); - return; - } - try { - data = cipher.doFinal(token.getBytes("UTF-8")); - } catch (IllegalBlockSizeException | BadPaddingException | UnsupportedEncodingException e1) { - // No JSON answer received - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_JSON); - return; - } - - byte[] ciphertext = new byte[data.length - AES_GCM_TAG_LENGTH / 8]; - byte[] gcmTag = new byte[AES_GCM_TAG_LENGTH / 8]; - System.arraycopy(data, 0, ciphertext, 0, data.length - AES_GCM_TAG_LENGTH / 8); - System.arraycopy(data, data.length - AES_GCM_TAG_LENGTH / 8, gcmTag, 0, AES_GCM_TAG_LENGTH / 8); - - JsonObject createSessionJsonObject = new JsonObject(); - createSessionJsonObject.addProperty("transactionId", transactionId); - createSessionJsonObject.addProperty("iv", Base64.getEncoder().encodeToString(iv)); - createSessionJsonObject.addProperty("tag", Base64.getEncoder().encodeToString(gcmTag)); - createSessionJsonObject.addProperty("payload", Base64.getEncoder().encodeToString(ciphertext)); - - // finally create the session for further communication - ContentResponse createSessionResponseContentResponse; - JsonObject createSessionResponseJsonObject; - try { - createSessionResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, - AUTH_CREATE_SESSION, createSessionJsonObject); - createSessionResponseJsonObject = ThirdGenerationHttpHelper - .getJsonObjectFromResponse(createSessionResponseContentResponse); - } catch (InterruptedException | TimeoutException | ExecutionException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - COMMUNICATION_ERROR_AUTHENTICATION); - return; - } - // 200 is the desired status code - if (createSessionResponseContentResponse.getStatus() == 400) { - // Authentication failed - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - CONFIGURATION_ERROR_PASSWORD); - return; - } - - sessionId = createSessionResponseJsonObject.get("sessionId").getAsString(); - - updateStatus(ThingStatus.ONLINE); - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHttpHelper.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHttpHelper.java deleted file mode 100644 index c20a995a2f..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationHttpHelper.java +++ /dev/null @@ -1,157 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.http.HttpVersion; - -import com.google.gson.FieldNamingPolicy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -/** - * The {@link ThirdGenerationHttpHelper} is handling http communication with the device - * handlers. - * - * @author René Stakemeier - Initial contribution - */ -final class ThirdGenerationHttpHelper { - - private ThirdGenerationHttpHelper() { - } - - // base URL of the web api - private static final String WEB_API = "/api/v1"; - // GSON handler - private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - - /** - * Helper function to execute a HTTP post request - * - * @param httpClient httpClient to use for communication - * @param url IP or hostname or the device - * @param resource web API resource to post to - * @param parameters the JSON content to post - * @return the HTTP response for the created post request - * @throws ExecutionException Error during the execution of the http request - * @throws TimeoutException Connection timed out - * @throws InterruptedException Connection interrupted - */ - static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonObject parameters) - throws InterruptedException, TimeoutException, ExecutionException { - return executeHttpPost(httpClient, url, resource, parameters, null); - } - - /** - * Helper function to execute a HTTP post request - * - * @param httpClient httpClient to use for communication - * @param url IP or hostname or the device - * @param resource web API resource to post to - * @param sessionId optional session ID - * @param parameters the JSON content to post - * @return the HTTP response for the created post request - * @throws ExecutionException Error during the execution of the http request - * @throws TimeoutException Connection timed out - * @throws InterruptedException Connection interrupted - */ - static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonElement parameters, - @Nullable String sessionId) throws InterruptedException, TimeoutException, ExecutionException { - Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http") - .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.POST) - .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json") - .timeout(5, TimeUnit.SECONDS); - response.content(new StringContentProvider(parameters.toString())); - if (sessionId != null) { - response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId)); - } - return response.send(); - } - - /** - * Helper function to execute a HTTP get request - * - * @param httpClient httpClient to use for communication - * @param url IP or hostname or the device - * @param resource web API resource to get - * @return the HTTP response for the created get request - * @throws ExecutionException Error during the execution of the http request - * @throws TimeoutException Connection timed out - * @throws InterruptedException Connection interrupted - */ - static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource) - throws InterruptedException, TimeoutException, ExecutionException { - return executeHttpGet(httpClient, url, resource, null); - } - - /** - * Helper function to execute a HTTP get request - * - * @param httpClient httpClient to use for communication - * @param url IP or hostname or the device - * @param resource web API resource to get - * @param sessionId optional session ID - * @return the HTTP response for the created get request - * @throws ExecutionException Error during the execution of the http request - * @throws TimeoutException Connection timed out - * @throws InterruptedException Connection interrupted - * @throws Exception thrown if there are communication problems - */ - static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource, - @Nullable String sessionId) throws InterruptedException, TimeoutException, ExecutionException { - Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http") - .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.GET) - .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json") - .timeout(5, TimeUnit.SECONDS); - if (sessionId != null) { - response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId)); - } - return response.send(); - } - - /** - * Helper to extract the JsonArray from a HTTP response. - * Use only, if you expect a JsonArray and no other types (e.g. JSON array)! - * - * @param reponse the HTTP response - * @return the JSON object - */ - static JsonArray getJsonArrayFromResponse(ContentResponse reponse) { - return GSON.fromJson(reponse.getContentAsString(), JsonArray.class); - } - - /** - * Helper to extract the JSON object from a HTTP response. - * Use only, if you expect a JSON object and no other types (e.g. JSON array)! - * - * @param reponse the HTTP response - * @return the JSON object - */ - static JsonObject getJsonObjectFromResponse(ContentResponse reponse) { - return GSON.fromJson(reponse.getContentAsString(), JsonObject.class); - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationInverterTypes.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationInverterTypes.java deleted file mode 100644 index c8cefc9ec5..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationInverterTypes.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -/** - * The {@link ThirdGenerationInverterTypes} contains the list of supported devices - * Each one is represented by a dedicated thing - * - * @author René Stakemeier - Initial contribution - */ -public enum ThirdGenerationInverterTypes { - PLENTICORE_PLUS_42_WITH_BATTERY, - PLENTICORE_PLUS_42_WITHOUT_BATTERY, - PLENTICORE_PLUS_55_WITH_BATTERY, - PLENTICORE_PLUS_55_WITHOUT_BATTERY, - PLENTICORE_PLUS_70_WITH_BATTERY, - PLENTICORE_PLUS_70_WITHOUT_BATTERY, - PLENTICORE_PLUS_85_WITH_BATTERY, - PLENTICORE_PLUS_85_WITHOUT_BATTERY, - PLENTICORE_PLUS_100_WITH_BATTERY, - PLENTICORE_PLUS_100_WITHOUT_BATTERY, - PIKOIQ_42, - PIKOIQ_55, - PIKOIQ_70, - PIKOIQ_85, - PIKOIQ_100 -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationMappingInverterToChannel.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationMappingInverterToChannel.java deleted file mode 100644 index 6197e947f3..0000000000 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/internal/kostal/inverter/thirdgeneration/ThirdGenerationMappingInverterToChannel.java +++ /dev/null @@ -1,249 +0,0 @@ -/** - * 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.internal.kostal.inverter.thirdgeneration; - -import static org.openhab.binding.internal.kostal.inverter.thirdgeneration.ThirdGenerationBindingConstants.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * The {@link ThirdGenerationMappingInverterToChannel} is responsible for the management of the channels - * available to inverters - * - * @author René Stakemeier - Initial contribution - */ -class ThirdGenerationMappingInverterToChannel { - - private static final Map> CHANNEL_MAPPING = new HashMap<>(); - - /* - * Assign the channels to the devices. - */ - static { - List allInvertersList = Arrays.asList(ThirdGenerationInverterTypes.values()); - List allBatteryInvertersList = Stream - .of(ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITH_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITH_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITH_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITH_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITH_BATTERY) - .collect(Collectors.toList()); - List allInvertersWithThreeStringsList = Stream - .of(ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITHOUT_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITHOUT_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITHOUT_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITHOUT_BATTERY, - ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITHOUT_BATTERY) - .collect(Collectors.toList()); - - // Channels available on all devices - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_DC_POWER, "devices:local", "Dc_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_GRID, "devices:local", - "HomeGrid_P", ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_OWNCONSUMPTION, "devices:local", "HomeOwn_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_PV, "devices:local", "HomePv_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_TOTAL, "devices:local", "Home_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_LIMIT_EVU_ABSOLUTE, "devices:local", "LimitEvuAbs", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_LIMIT_EVU_RELATIV, "devices:local", "LimitEvuRel", - ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_WORKTIME, "devices:local", "WorkTime", - ThirdGenerationChannelDatatypes.SECONDS); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE, "devices:local:ac", - "L1_I", ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER, "devices:local:ac", "L1_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_VOLTAGE, "devices:local:ac", - "L1_U", ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_AMPERAGE, "devices:local:ac", - "L2_I", ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_POWER, "devices:local:ac", "L2_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_VOLTAGE, "devices:local:ac", - "L2_U", ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_AMPERAGE, "devices:local:ac", - "L3_I", ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_POWER, "devices:local:ac", "L3_P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_VOLTAGE, "devices:local:ac", - "L3_U", ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_CURRENT_POWER, "devices:local:ac", "P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_AMPERAGE, "devices:local:pv1", "I", - ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_POWER, "devices:local:pv1", "P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_VOLTAGE, "devices:local:pv1", "U", - ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_AMPERAGE, "devices:local:pv2", "I", - ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_POWER, "devices:local:pv2", "P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_VOLTAGE, "devices:local:pv2", "U", - ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_MC, "scb:event", "ErrMc", - ThirdGenerationChannelDatatypes.INTEGER); - addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_SFH, "scb:event", "ErrSFH", - ThirdGenerationChannelDatatypes.INTEGER); - addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_SCB, "scb:event", "Event:ActiveErrorCnt", - ThirdGenerationChannelDatatypes.INTEGER); - addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_WARNING_COUNT_SCB, "scb:event", "Event:ActiveWarningCnt", - ThirdGenerationChannelDatatypes.INTEGER); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_DAY, "scb:statistic:EnergyFlow", - "Statistic:Autarky:Day", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_MONTH, "scb:statistic:EnergyFlow", - "Statistic:Autarky:Month", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_TOTAL, "scb:statistic:EnergyFlow", - "Statistic:Autarky:Total", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_YEAR, "scb:statistic:EnergyFlow", - "Statistic:Autarky:Year", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_DAY, "scb:statistic:EnergyFlow", - "Statistic:CO2Saving:Day", ThirdGenerationChannelDatatypes.KILOGRAM); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_MONTH, "scb:statistic:EnergyFlow", - "Statistic:CO2Saving:Month", ThirdGenerationChannelDatatypes.KILOGRAM); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_TOTAL, "scb:statistic:EnergyFlow", - "Statistic:CO2Saving:Total", ThirdGenerationChannelDatatypes.KILOGRAM); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_YEAR, "scb:statistic:EnergyFlow", - "Statistic:CO2Saving:Year", ThirdGenerationChannelDatatypes.KILOGRAM); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_DAY, "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_MONTH, "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Month", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_TOTAL, "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Total", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_YEAR, "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_DAY, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Day", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_MONTH, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Month", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_TOTAL, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Total", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_YEAR, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Year", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_DAY, "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_MONTH, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomePv:Month", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_TOTAL, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomePv:Total", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_YEAR, "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_DAY, "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Day", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_MONTH, "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Month", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_TOTAL, "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Total", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_YEAR, "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Year", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_DAY, "scb:statistic:EnergyFlow", - "Statistic:Yield:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_MONTH, "scb:statistic:EnergyFlow", - "Statistic:Yield:Month", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_TOTAL, "scb:statistic:EnergyFlow", - "Statistic:Yield:Total", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_YEAR, "scb:statistic:EnergyFlow", - "Statistic:Yield:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - - // Plenticore Plus devices can be expanded with a battery. - // Additional channels become available, but the pv3 information are hidden (since the battery is attached - // there) - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_LOADING_CYCLES, - "devices:local:battery", "Cycles", ThirdGenerationChannelDatatypes.INTEGER); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_FULL_CHARGE_CAPACITY, - "devices:local:battery", "FullChargeCap_E", ThirdGenerationChannelDatatypes.AMPERE_HOUR); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_AMPERAGE, "devices:local:battery", "I", - ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_POWER, "devices:local:battery", "P", - ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_STATE_OF_CHARGE, - "devices:local:battery", "SoC", ThirdGenerationChannelDatatypes.PERCEMTAGE); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_VOLTAGE, "devices:local:battery", "U", - ThirdGenerationChannelDatatypes.VOLT); - addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_DAY, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Day", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_MONTH, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Month", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_TOTAL, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Total", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_YEAR, - "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Year", - ThirdGenerationChannelDatatypes.KILOWATT_HOUR); - addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_BATTERY, "devices:local", - "HomeBat_P", ThirdGenerationChannelDatatypes.WATT); - // Plenticore devices without battery have the pv3 settings available - addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_AMPERAGE, - "devices:local:pv3", "I", ThirdGenerationChannelDatatypes.AMPERE); - addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_POWER, "devices:local:pv3", - "P", ThirdGenerationChannelDatatypes.WATT); - addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_VOLTAGE, - "devices:local:pv3", "U", ThirdGenerationChannelDatatypes.VOLT); - } - - static Map> getModuleToChannelsMappingForInverter( - ThirdGenerationInverterTypes inverter) { - Map> results = new HashMap<>(); - for (ThirdGenerationChannelMappingToWebApi mapping : CHANNEL_MAPPING.get(inverter)) { - List channelList = null; - if (results.containsKey(mapping.moduleId)) { - channelList = results.get(mapping.moduleId); - } else { - channelList = new ArrayList<>(); - results.put(mapping.moduleId, channelList); - } - channelList.add(mapping); - } - return results; - } - - private static void addInverterChannel(ThirdGenerationInverterTypes inverter, - ThirdGenerationChannelMappingToWebApi mapping) { - if (!CHANNEL_MAPPING.containsKey(inverter)) { - CHANNEL_MAPPING.put(inverter, new ArrayList<>()); - } - CHANNEL_MAPPING.get(inverter).add(mapping); - } - - private static void addInverterChannel(ThirdGenerationInverterTypes inverter, String channelUID, String moduleId, - String processdataId, ThirdGenerationChannelDatatypes dataType) { - addInverterChannel(inverter, - new ThirdGenerationChannelMappingToWebApi(channelUID, moduleId, processdataId, dataType)); - } - - private static void addInverterChannel(List inverterList, String channelUID, - String moduleId, String processdataId, ThirdGenerationChannelDatatypes dataType) { - for (ThirdGenerationInverterTypes inverter : inverterList) { - addInverterChannel(inverter, channelUID, moduleId, processdataId, dataType); - } - } -} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/KostalInverterFactory.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/KostalInverterFactory.java new file mode 100644 index 0000000000..79a00e2912 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/KostalInverterFactory.java @@ -0,0 +1,102 @@ +/** + * 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.kostalinverter.internal; + +import static org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationBindingConstants.*; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.kostalinverter.internal.firstgeneration.WebscrapeHandler; +import org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationHandler; +import org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationInverterTypes; +import org.openhab.core.io.net.http.HttpClientFactory; +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.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * @author Christian Schneider - Initial contribution (as WebscrapeHandlerFactory.java) + * @author René Stakemeier - extension for the third generation of KOSTAL inverters + */ +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.kostalinverter") +@NonNullByDefault +public class KostalInverterFactory extends BaseThingHandlerFactory { + + private static final Map SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS = new HashMap<>(); + static { + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ42, ThirdGenerationInverterTypes.PIKOIQ_42); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ55, ThirdGenerationInverterTypes.PIKOIQ_55); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ70, ThirdGenerationInverterTypes.PIKOIQ_70); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ85, ThirdGenerationInverterTypes.PIKOIQ_85); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PIKOIQ100, ThirdGenerationInverterTypes.PIKOIQ_100); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS42WITHBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITH_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS55WITHBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITH_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS70WITHBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITH_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS85WITHBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITH_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS100WITHBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITH_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS42WITHOUTBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITHOUT_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS55WITHOUTBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITHOUT_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS70WITHOUTBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITHOUT_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS85WITHOUTBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITHOUT_BATTERY); + SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.put(PLENTICOREPLUS100WITHOUTBATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITHOUT_BATTERY); + } + + public static final ThingTypeUID FIRST_GENERATION_INVERTER = new ThingTypeUID("kostalinverter", "kostalinverter"); + + private final HttpClient httpClient; + + @Activate + public KostalInverterFactory(@Reference final HttpClientFactory httpClientFactory) { + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return thingTypeUID.equals(FIRST_GENERATION_INVERTER) + || SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS.keySet().contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + // first generation + if (FIRST_GENERATION_INVERTER.equals(thing.getThingTypeUID())) { + return new WebscrapeHandler(thing); + } + // third generation + ThirdGenerationInverterTypes inverterType = SUPPORTED_THIRD_GENERATION_THING_TYPES_UIDS + .get(thing.getThingTypeUID()); + if (inverterType != null) { + return new ThirdGenerationHandler(thing, httpClient, inverterType); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/ChannelConfig.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/ChannelConfig.java new file mode 100644 index 0000000000..2216753182 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/ChannelConfig.java @@ -0,0 +1,33 @@ +/** + * 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.kostalinverter.internal.firstgeneration; + +import javax.measure.Unit; + +/** + * @author Christian Schneider - Initial contribution + * @author Christoph Weitkamp - Incorporated new QuantityType (Units of Measurement) + */ +public class ChannelConfig { + public ChannelConfig(String id, String tag, int num, Unit unit) { + this.id = id; + this.tag = tag; + this.num = num; + this.unit = unit; + } + + String id; + String tag; + int num; + Unit unit; +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/SourceConfig.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/SourceConfig.java new file mode 100644 index 0000000000..deec9e7415 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/SourceConfig.java @@ -0,0 +1,23 @@ +/** + * 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.kostalinverter.internal.firstgeneration; + +/** + * @author Christian Schneider - Initial contribution + */ +public class SourceConfig { + public String url; + public String userName; + public String password; + public int refreshInterval; +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/WebscrapeHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/WebscrapeHandler.java new file mode 100644 index 0000000000..c635db3069 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/firstgeneration/WebscrapeHandler.java @@ -0,0 +1,133 @@ +/** + * 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.kostalinverter.internal.firstgeneration; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; +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.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Christian Schneider - Initial contribution + * @author Christoph Weitkamp - Incorporated new QuantityType (Units of Measurement) + */ +public class WebscrapeHandler extends BaseThingHandler { + private Logger logger = LoggerFactory.getLogger(WebscrapeHandler.class); + private SourceConfig config; + + private final List channelConfigs = new ArrayList<>(); + + public WebscrapeHandler(Thing thing) { + super(thing); + channelConfigs.add(new ChannelConfig("acPower", "td", 4, Units.WATT)); + channelConfigs.add(new ChannelConfig("totalEnergy", "td", 7, Units.KILOWATT_HOUR)); + channelConfigs.add(new ChannelConfig("dayEnergy", "td", 10, Units.KILOWATT_HOUR)); + channelConfigs.add(new ChannelConfig("status", "td", 13, null)); + channelConfigs.add(new ChannelConfig("str1Voltage", "td", 19, Units.VOLT)); + channelConfigs.add(new ChannelConfig("str1Current", "td", 25, Units.AMPERE)); + channelConfigs.add(new ChannelConfig("str2Voltage", "td", 33, Units.VOLT)); + channelConfigs.add(new ChannelConfig("str2Current", "td", 39, Units.AMPERE)); + channelConfigs.add(new ChannelConfig("l1Voltage", "td", 22, Units.VOLT)); + channelConfigs.add(new ChannelConfig("l1Power", "td", 28, Units.WATT)); + channelConfigs.add(new ChannelConfig("l2Voltage", "td", 36, Units.VOLT)); + channelConfigs.add(new ChannelConfig("l2Power", "td", 42, Units.WATT)); + channelConfigs.add(new ChannelConfig("l3Voltage", "td", 46, Units.VOLT)); + channelConfigs.add(new ChannelConfig("l3Power", "td", 49, Units.WATT)); + } + + @Override + public void initialize() { + config = getConfigAs(SourceConfig.class); + scheduler.scheduleWithFixedDelay(() -> { + try { + refresh(); + updateStatus(ThingStatus.ONLINE); + } catch (Exception e) { + logger.debug("Error refreshing source '{}'", getThing().getUID(), e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + e.getClass().getName() + ":" + e.getMessage()); + } + }, 0, config.refreshInterval, TimeUnit.SECONDS); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // Read only + } + + private void refresh() throws Exception { + Document doc = getDoc(); + for (ChannelConfig cConfig : channelConfigs) { + Channel channel = getThing().getChannel(cConfig.id); + if (channel != null) { + String value = getTag(doc, cConfig.tag).get(cConfig.num); + updateState(channel.getUID(), getState(value, cConfig.unit)); + } + } + } + + private static List getTag(Document doc, String tag) { + List result = new ArrayList<>(); + Iterator elIt = doc.getElementsByTag(tag).iterator(); + while (elIt.hasNext()) { + String content = elIt.next().text(); + content = content.replace("\u00A0", "").trim(); + if (!content.isEmpty()) { + result.add(content); + } + } + return result; + } + + private Document getDoc() throws IOException { + String login = config.userName + ":" + config.password; + String base64login = new String(Base64.getEncoder().encode(login.getBytes())); + return Jsoup.connect(config.url).header("Authorization", "Basic " + base64login).get(); + } + + private State getState(String value, Unit unit) { + if (unit == null) { + return new StringType(value); + } else { + try { + return new QuantityType<>(new BigDecimal(value), unit); + } catch (NumberFormatException e) { + logger.debug("Error parsing value '{}'", value, e); + return UnDefType.UNDEF; + } + } + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java new file mode 100644 index 0000000000..d279b706d1 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java @@ -0,0 +1,144 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link ThirdGenerationBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author René Stakemeier - Initial contribution + */ +@NonNullByDefault +public class ThirdGenerationBindingConstants { + + private static final String BINDING_ID = "kostalinverter"; + + // List of all constants used for the authentication + static final String USER_TYPE = "user"; + static final String HMAC_SHA256_ALGORITHM = "HMACSHA256"; + static final String SHA_256_HASH = "SHA-256"; + static final int AES_GCM_TAG_LENGTH = 128; // bit count + + // List of all Thing Type UIDs + public static final ThingTypeUID PIKOIQ42 = new ThingTypeUID(BINDING_ID, "PIKOIQ42"); + public static final ThingTypeUID PIKOIQ55 = new ThingTypeUID(BINDING_ID, "PIKOIQ55"); + public static final ThingTypeUID PIKOIQ70 = new ThingTypeUID(BINDING_ID, "PIKOIQ70"); + public static final ThingTypeUID PIKOIQ85 = new ThingTypeUID(BINDING_ID, "PIKOIQ85"); + public static final ThingTypeUID PIKOIQ100 = new ThingTypeUID(BINDING_ID, "PIKOIQ100"); + public static final ThingTypeUID PLENTICOREPLUS42WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS42WITHOUTBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS55WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS55WITHOUTBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS70WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS70WITHOUTBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS85WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS85WITHOUTBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS100WITHOUTBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS100WITHOUTBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS42WITHBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS42WITHBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS55WITHBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS55WITHBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS70WITHBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS70WITHBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS85WITHBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS85WITHBATTERY"); + public static final ThingTypeUID PLENTICOREPLUS100WITHBATTERY = new ThingTypeUID(BINDING_ID, + "PLENTICOREPLUS100WITHBATTERY"); + + // List of error messages + public static final String COMMUNICATION_ERROR_AUTHENTICATION = "Error during the initialisation of the authentication"; + public static final String COMMUNICATION_ERROR_UNSUPPORTED_ENCODING = "The text encoding is not supported by this openHAB installation"; + public static final String COMMUNICATION_ERROR_AES_ERROR = "The java installation does not support AES encryption in GCM mode"; + public static final String COMMUNICATION_ERROR_HTTP = "HTTP communication error: No response from device"; + public static final String COMMUNICATION_ERROR_JSON = "HTTP communication error: answer did not match the expected format"; + public static final String COMMUNICATION_ERROR_API_CHANGED = "The API seems to have changed :-( Maybe this implementation has become incompatible with the device"; + public static final String COMMUNICATION_ERROR_INCOMPATIBLE_DEVICE = "The device could not provide the required information. Please check, if you selected the right thing for your device!"; + public static final String COMMUNICATION_ERROR_USER_ACCOUNT_LOCKED = "Your user account on the device is locked. Please reset the password by following the instructions on the device´s web frontend"; + public static final String CONFIGURATION_ERROR_PASSWORD = "Wrong password"; + + // List of all Channel uids + public static final String CHANNEL_DEVICE_LOCAL_DC_POWER = "deviceLocalDCPower"; + public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_BATTERY = "deviceLocalHomeconsumptionFromBattery"; + public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_GRID = "deviceLocalHomeconsumptionFromGrid"; + public static final String CHANNEL_DEVICE_LOCAL_OWNCONSUMPTION = "deviceLocalOwnconsumption"; + public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_PV = "deviceLocalHomeconsumptionFromPV"; + public static final String CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_TOTAL = "deviceLocalHomeconsumptionTotal"; + public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_ABSOLUTE = "deviceLocalLimitEVUAbsolute"; + public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_RELATIV = "deviceLocalLimitEVURelativ"; + public static final String CHANNEL_DEVICE_LOCAL_WORKTIME = "deviceLocalWorktime"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE = "deviceLocalACPhase1CurrentAmperage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER = "deviceLocalACPhase1CurrentPower"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_VOLTAGE = "deviceLocalACPhase1CurrentVoltage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_AMPERAGE = "deviceLocalACPhase2CurrentAmperage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_POWER = "deviceLocalACPhase2CurrentPower"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_VOLTAGE = "deviceLocalACPhase2CurrentVoltage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_AMPERAGE = "deviceLocalACPhase3CurrentAmperage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_POWER = "deviceLocalACPhase3CurrentPower"; + public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_VOLTAGE = "deviceLocalACPhase3CurrentVoltage"; + public static final String CHANNEL_DEVICE_LOCAL_AC_CURRENT_POWER = "deviceLocalACCurrentPower"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_LOADING_CYCLES = "deviceLocalBatteryLoadingCycles"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_FULL_CHARGE_CAPACITY = "deviceLocalBatteryFullChargeCapacity"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_AMPERAGE = "deviceLocalBatteryAmperage"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_POWER = "deviceLocalBatteryPower"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_STATE_OF_CHARGE = "deviceLocalBatteryStageOfCharge"; + public static final String CHANNEL_DEVICE_LOCAL_BATTERY_VOLTAGE = "deviceLocalBatteryVoltage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_AMPERAGE = "deviceLocalPVString1Amperage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_POWER = "deviceLocalPVString1Power"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_1_VOLTAGE = "deviceLocalPVString1Voltage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_AMPERAGE = "deviceLocalPVString2Amperage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_POWER = "deviceLocalPVString2Power"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_2_VOLTAGE = "deviceLocalPVString2Voltage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_AMPERAGE = "deviceLocalPVString3Amperage"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_POWER = "deviceLocalPVString3Power"; + public static final String CHANNEL_DEVICE_LOCAL_PVSTRING_3_VOLTAGE = "deviceLocalPVString3Voltage"; + public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_MC = "SCBEventErrorCountMc"; + public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_SFH = "SCBEventErrorCountSFH"; + public static final String CHANNEL_SCB_EVENT_ERROR_COUNT_SCB = "SCBEventErrorCountSCB"; + public static final String CHANNEL_SCB_EVENT_WARNING_COUNT_SCB = "SCBEventWarningCountSCB"; + public static final String CHANNEL_STATISTIC_AUTARKY_DAY = "statisticAutarkyDay"; + public static final String CHANNEL_STATISTIC_AUTARKY_MONTH = "statisticAutarkyMonth"; + public static final String CHANNEL_STATISTIC_AUTARKY_TOTAL = "statisticAutarkyTotal"; + public static final String CHANNEL_STATISTIC_AUTARKY_YEAR = "statisticAutarkyYear"; + public static final String CHANNEL_STATISTIC_CO2SAVING_DAY = "statisticCo2SavingDay"; + public static final String CHANNEL_STATISTIC_CO2SAVING_MONTH = "statisticCo2SavingMonth"; + public static final String CHANNEL_STATISTIC_CO2SAVING_TOTAL = "statisticCo2SavingTotal"; + public static final String CHANNEL_STATISTIC_CO2SAVING_YEAR = "statisticCo2SavingYear"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_DAY = "statisticHomeconsumptionDay"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_MONTH = "statisticHomeconsumptionMonth"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_TOTAL = "statisticHomeconsumptionTotal"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_YEAR = "statisticHomeconsumptionYear"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_DAY = "statisticHomeconsumptionFromBatteryDay"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_MONTH = "statisticHomeconsumptionFromBatteryMonth"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_TOTAL = "statisticHomeconsumptionFromBatteryTotal"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_YEAR = "statisticHomeconsumptionFromBatteryYear"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_DAY = "statisticHomeconsumptionFromGridDay"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_MONTH = "statisticHomeconsumptionFromGridMonth"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_TOTAL = "statisticHomeconsumptionFromGridTotal"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_YEAR = "statisticHomeconsumptionFromGridYear"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_DAY = "statisticHomeconsumptionFromPVDay"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_MONTH = "statisticHomeconsumptionFromPVMonth"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_TOTAL = "statisticHomeconsumptionFromPVTotal"; + public static final String CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_YEAR = "statisticHomeconsumptionFromPVYear"; + public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_DAY = "statisticOwnconsumptionRateDay"; + public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_MONTH = "statisticOwnconsumptionRateMonth"; + public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_TOTAL = "statisticOwnconsumptionRateTotal"; + public static final String CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_YEAR = "statisticOwnconsumptionRateYear"; + public static final String CHANNEL_STATISTIC_YIELD_DAY = "statisticYieldDay"; + public static final String CHANNEL_STATISTIC_YIELD_MONTH = "statisticYieldMonth"; + public static final String CHANNEL_STATISTIC_YIELD_TOTAL = "statisticYieldTotal"; + public static final String CHANNEL_STATISTIC_YIELD_YEAR = "statisticYieldYear"; +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java new file mode 100644 index 0000000000..75b4e023d1 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java @@ -0,0 +1,30 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +/** + * The {@link ThirdGenerationChannelDatatypes} enumeration contains the data types provided by the device + * + * @author René Stakemeier - Initial contribution + */ +enum ThirdGenerationChannelDatatypes { + INTEGER, + PERCEMTAGE, + KILOGRAM, + SECONDS, + KILOWATT_HOUR, + WATT, + AMPERE, + AMPERE_HOUR, + VOLT +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java new file mode 100644 index 0000000000..b74b31a33a --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelMappingToWebApi.java @@ -0,0 +1,43 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +/*** + * The {@link ThirdGenerationChannelMappingToWebApi}} is used to map the channel name to the web API commands + * + * @author René Stakemeier - Initial contribution + * + */ +class ThirdGenerationChannelMappingToWebApi { + + String channelUID; + String moduleId; + String processdataId; + ThirdGenerationChannelDatatypes dataType; + + /** + * Constructor of {@link ThirdGenerationChannelMappingToWebApi} + * + * @param channelUID The channel UUID + * @param moduleId module id (as defined by the web api) + * @param processdataId process data id (as defined by the web api) + * @param dataType data type of this channel + */ + ThirdGenerationChannelMappingToWebApi(String channelUID, String moduleId, String processdataId, + ThirdGenerationChannelDatatypes dataType) { + this.channelUID = channelUID; + this.moduleId = moduleId; + this.processdataId = processdataId; + this.dataType = dataType; + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationConfiguration.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationConfiguration.java new file mode 100644 index 0000000000..9683751f69 --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationConfiguration.java @@ -0,0 +1,25 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +/** + * The {@link ThirdGenerationConfiguration} class contains fields mapping thing configuration parameters. + * + * @author René Stakemeier - Initial contribution + */ +public class ThirdGenerationConfiguration { + + public String url; + public String userPassword; + public int refreshInternalInSeconds; +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationEncryptionHelper.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationEncryptionHelper.java new file mode 100644 index 0000000000..7c3a07eeaf --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationEncryptionHelper.java @@ -0,0 +1,121 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +import static org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationBindingConstants.*; + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import java.util.Random; + +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * The {@link ThirdGenerationEncryptionHelper} is responsible for handling the encryption for the authentication + * handlers. + * + * @author René Stakemeier - Initial contribution + */ +final class ThirdGenerationEncryptionHelper { + + private ThirdGenerationEncryptionHelper() { + } + + /** + * This method generates the HMACSha256 encrypted value of the given value + * + * @param password Password used for encryption + * @param valueToEncrypt value to encrypt + * @return encrypted value + * @throws InvalidKeyException thrown if the key generated from the password is invalid + * @throws NoSuchAlgorithmException thrown if HMAC SHA 256 is not supported + */ + static byte[] getHMACSha256(byte[] password, String valueToEncrypt) + throws InvalidKeyException, NoSuchAlgorithmException { + SecretKeySpec signingKey = new SecretKeySpec(password, HMAC_SHA256_ALGORITHM); + Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); + mac.init(signingKey); + mac.update(valueToEncrypt.getBytes()); + return mac.doFinal(); + } + + /** + * This methods generates the client proof. + * It is calculated as XOR between the {@link clientSignature} and the {@link serverSignature} + * + * @param clientSignature client signature + * @param serverSignature server signature + * @return client proof + */ + static String createClientProof(byte[] clientSignature, byte[] serverSignature) { + byte[] result = new byte[clientSignature.length]; + for (int i = 0; i < clientSignature.length; i++) { + result[i] = (byte) (0xff & (clientSignature[i] ^ serverSignature[i])); + } + return Base64.getEncoder().encodeToString(result); + } + + /** + * Create the PBKDF2 hash + * + * @param password password + * @param salt salt + * @param rounds rounds + * @return hash + * @throws NoSuchAlgorithmException if PBKDF2WithHmacSHA256 is not supported + * @throws InvalidKeySpecException if the key specification is not supported + */ + static byte[] getPBKDF2Hash(String password, byte[] salt, int rounds) + throws NoSuchAlgorithmException, InvalidKeySpecException { + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, rounds, 256); + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + return skf.generateSecret(spec).getEncoded(); + } + + /** + * Create the SHA256 hash value for the given byte array + * + * @param valueToHash byte array to get the hash value for + * @return the hash value + * @throws NoSuchAlgorithmException if SHA256 is not supported + */ + static byte[] getSha256Hash(byte[] valueToHash) throws NoSuchAlgorithmException { + return MessageDigest.getInstance(SHA_256_HASH).digest(valueToHash); + } + + /** + * Create the nonce (numbers used once) for the client for communication + * + * @return nonce + */ + static String createClientNonce() { + Random generator = new Random(); + + // Randomize the random generator + byte[] randomizeArray = new byte[1024]; + generator.nextBytes(randomizeArray); + + // 3 words of 4 bytes are required for the handshake + byte[] nonceArray = new byte[12]; + generator.nextBytes(nonceArray); + + // return the base64 encoded value of the random words + return Base64.getMimeEncoder().encodeToString(nonceArray); + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java new file mode 100644 index 0000000000..b4c16e004f --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java @@ -0,0 +1,485 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +import static org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationBindingConstants.*; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * The {@link ThirdGenerationHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author René Stakemeier - Initial contribution + */ +@NonNullByDefault +public class ThirdGenerationHandler extends BaseThingHandler { + + /* + * operations used for authentication + */ + private static final String AUTH_START = "/auth/start"; + private static final String AUTH_FINISH = "/auth/finish"; + private static final String AUTH_CREATE_SESSION = "/auth/create_session"; + + /* + * operations used for gathering process data from the device + */ + private static final String PROCESSDATA = "/processdata"; + + /* + * After the authentication the result (the session id) is stored here and used to "sign" future requests + */ + private @Nullable String sessionId; + /* + * The configuration file containing the host, the password and the refresh interval + */ + private @NonNullByDefault({}) ThirdGenerationConfiguration config; + + private @Nullable ScheduledFuture refreshScheduler; + + private @Nullable HttpClient httpClient; + + private ThirdGenerationInverterTypes inverterType; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * Constructor of this class + * + * @param thing the thing + * @param httpClient the httpClient used for communication + * @param inverterType the type of the device + */ + public ThirdGenerationHandler(Thing thing, HttpClient httpClient, ThirdGenerationInverterTypes inverterType) { + super(thing); + this.inverterType = inverterType; + this.httpClient = httpClient; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // All channels are readonly and updated by the scheduler + } + + @Override + public void dispose() { + if (refreshScheduler != null) { + refreshScheduler.cancel(true); + refreshScheduler = null; + } + super.dispose(); + } + + @Override + public void initialize() { + config = getConfigAs(ThirdGenerationConfiguration.class); + // temporary value while initializing + updateStatus(ThingStatus.UNKNOWN); + + // Start the authentication + scheduler.schedule(this::authenticate, 1, TimeUnit.SECONDS); + + // Start the update scheduler as configured + refreshScheduler = scheduler.scheduleWithFixedDelay(this::updateChannelValues, 10, + config.refreshInternalInSeconds, TimeUnit.SECONDS); + } + + /** + * The API supports the resolution of multiple values at a time + * + * Therefore this methods builds one request to gather all information for the current inverter. + * The list contains all channels as defined in {@link ThirdGenerationMappingInverterToChannel} for the + * current inverter + * + */ + private void updateChannelValues() { + Map> channelList = ThirdGenerationMappingInverterToChannel + .getModuleToChannelsMappingForInverter(inverterType); + JsonArray updateMessageJsonArray = getUpdateChannelMessage(channelList); + + // Send the API request to get values for all channels + ContentResponse updateMessageContentResponse; + try { + updateMessageContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, + PROCESSDATA, updateMessageJsonArray, sessionId); + if (updateMessageContentResponse.getStatus() == 404) { + // Module not found + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_INCOMPATIBLE_DEVICE); + return; + } + if (updateMessageContentResponse.getStatus() == 503) { + // Communication error (e.g. during initial boot of the SCB) + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_HTTP); + return; + } + if (updateMessageContentResponse.getStatus() == 401) { + // session not valid (timed out? device rebooted?) + logger.info("Session expired - performing retry"); + authenticate(); + // Retry + updateMessageContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, + PROCESSDATA, updateMessageJsonArray, sessionId); + } + } catch (TimeoutException | ExecutionException e) { + // Communication problem + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); + return; + } + JsonArray updateMessageResultsJsonArray = ThirdGenerationHttpHelper + .getJsonArrayFromResponse(updateMessageContentResponse); + + // Map the returned values back to the channels and update them + for (int i = 0; i < updateMessageResultsJsonArray.size(); i++) { + JsonObject moduleAnswer = updateMessageResultsJsonArray.get(i).getAsJsonObject(); + String moduleName = moduleAnswer.get("moduleid").getAsString(); + JsonArray processdata = moduleAnswer.get("processdata").getAsJsonArray(); + for (int j = 0; j < processdata.size(); j++) { + // Update the channels with their new value + JsonObject newValueObject = processdata.get(j).getAsJsonObject(); + String valueId = newValueObject.get("id").getAsString(); + double valueAsDouble = newValueObject.get("value").getAsDouble(); + ThirdGenerationChannelMappingToWebApi channel = channelList.get(moduleName).stream() + .filter(c -> c.moduleId.equals(moduleName) && c.processdataId.equals(valueId)).findFirst() + .get(); + updateChannelValue(channel.channelUID, channel.dataType, valueAsDouble); + } + } + updateStatus(ThingStatus.ONLINE); + } + + /** + * Update the channel to the given value. + * The value is set to the matching data (SITypes etc) + * + * @param channeluid Channel to update + * @param dataType target data type + * @param value value + */ + private void updateChannelValue(String channeluid, ThirdGenerationChannelDatatypes dataType, Double value) { + switch (dataType) { + case INTEGER: { + updateState(channeluid, new DecimalType(value.longValue())); + break; + } + case PERCEMTAGE: { + updateState(channeluid, new QuantityType<>(value, Units.PERCENT)); + break; + } + case KILOGRAM: { + updateState(channeluid, new QuantityType<>(value / 1000, SIUnits.KILOGRAM)); + break; + } + case SECONDS: { + updateState(channeluid, new QuantityType<>(value, Units.SECOND)); + break; + } + case KILOWATT_HOUR: { + updateState(channeluid, new QuantityType<>(value / 1000, Units.KILOWATT_HOUR)); + break; + } + case WATT: { + updateState(channeluid, new QuantityType<>(value, Units.WATT)); + break; + } + case AMPERE: { + updateState(channeluid, new QuantityType<>(value, Units.AMPERE)); + break; + } + case AMPERE_HOUR: { + // Ampere hours is not a supported unit, but 1 AH is equal tp 3600 coulomb... + updateState(channeluid, new QuantityType<>(value * 3600, Units.COULOMB)); + break; + } + case VOLT: { + updateState(channeluid, new QuantityType<>(value, Units.VOLT)); + break; + } + default: { + // unknown datatype + logger.debug("{} not known!", dataType); + } + } + } + + /** + * Creates the message which has to be send to the inverter to gather the current informations for all channels + * + * @param channelList channels of this thing + * @return the JSON array to send to the device + */ + private JsonArray getUpdateChannelMessage(Map> channelList) { + // Build the message to send to the inverter + JsonArray updateMessageJsonArray = new JsonArray(); + for (Entry> moduleId : channelList.entrySet()) { + JsonObject moduleJsonObject = new JsonObject(); + moduleJsonObject.addProperty("moduleid", moduleId.getKey()); + + JsonArray processdataNames = new JsonArray(); + for (ThirdGenerationChannelMappingToWebApi processdata : channelList.get(moduleId.getKey())) { + processdataNames.add(processdata.processdataId); + } + moduleJsonObject.add("processdataids", processdataNames); + updateMessageJsonArray.add(moduleJsonObject); + } + return updateMessageJsonArray; + } + + /** + * This function is used to authenticate against the SCB. + * SCB uses PBKDF2 and AES256 GCM mode with a slightly modified authentication message. + * The authentication will fail on JRE < 8u162. since the security policy is set to "limited" by default (see readme + * for fix) + */ + private final void authenticate() { + // Create random numbers + String clientNonce = ThirdGenerationEncryptionHelper.createClientNonce(); + // Perform first step of authentication + JsonObject authMeJsonObject = new JsonObject(); + authMeJsonObject.addProperty("username", USER_TYPE); + authMeJsonObject.addProperty("nonce", clientNonce); + + ContentResponse authStartResponseContentResponse; + try { + authStartResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, + AUTH_START, authMeJsonObject); + + // 200 is the desired status code + int statusCode = authStartResponseContentResponse.getStatus(); + + if (statusCode == 400) { + // Invalid user (which is hard coded and therefore can not be wrong until the api is changed by the + // manufacturer + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_API_CHANGED); + return; + } + if (statusCode == 403) { + // User is logged + // This can happen, if the user had to many bad attempts of entering the password in the web + // front end + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + COMMUNICATION_ERROR_USER_ACCOUNT_LOCKED); + return; + } + + if (statusCode == 503) { + // internal communication error + // This can happen if the device is not ready yet for communication + updateStatus(ThingStatus.UNINITIALIZED); + return; + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); + return; + } + JsonObject authMeResponseJsonObject = ThirdGenerationHttpHelper + .getJsonObjectFromResponse(authStartResponseContentResponse); + + // Extract information from the response + String salt = authMeResponseJsonObject.get("salt").getAsString(); + String serverNonce = authMeResponseJsonObject.get("nonce").getAsString(); + int rounds = authMeResponseJsonObject.get("rounds").getAsInt(); + String transactionId = authMeResponseJsonObject.get("transactionId").getAsString(); + + // Do the cryptography stuff (magic happens here) + byte[] saltedPasswort; + byte[] clientKey; + byte[] serverKey; + byte[] storedKey; + byte[] clientSignature; + byte[] serverSignature; + String authMessage; + try { + saltedPasswort = ThirdGenerationEncryptionHelper.getPBKDF2Hash(config.userPassword, + Base64.getDecoder().decode(salt), rounds); + clientKey = ThirdGenerationEncryptionHelper.getHMACSha256(saltedPasswort, "Client Key"); + serverKey = ThirdGenerationEncryptionHelper.getHMACSha256(saltedPasswort, "Server Key"); + storedKey = ThirdGenerationEncryptionHelper.getSha256Hash(clientKey); + authMessage = String.format("n=%s,r=%s,r=%s,s=%s,i=%d,c=biws,r=%s", USER_TYPE, clientNonce, serverNonce, + salt, rounds, serverNonce); + clientSignature = ThirdGenerationEncryptionHelper.getHMACSha256(storedKey, authMessage); + serverSignature = ThirdGenerationEncryptionHelper.getHMACSha256(serverKey, authMessage); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | IllegalStateException e2) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_AUTHENTICATION); + return; + } + String clientProof = ThirdGenerationEncryptionHelper.createClientProof(clientSignature, clientKey); + // Perform step 2 of the authentication + JsonObject authFinishJsonObject = new JsonObject(); + authFinishJsonObject.addProperty("transactionId", transactionId); + authFinishJsonObject.addProperty("proof", clientProof); + + ContentResponse authFinishResponseContentResponse; + JsonObject authFinishResponseJsonObject; + try { + authFinishResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, + AUTH_FINISH, authFinishJsonObject); + authFinishResponseJsonObject = ThirdGenerationHttpHelper + .getJsonObjectFromResponse(authFinishResponseContentResponse); + // 200 is the desired status code + if (authFinishResponseContentResponse.getStatus() == 400) { + // Authentication failed + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + CONFIGURATION_ERROR_PASSWORD); + return; + } + } catch (InterruptedException | TimeoutException | ExecutionException e3) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_HTTP); + return; + } + + // Extract information from the response + byte[] signature = Base64.getDecoder().decode(authFinishResponseJsonObject.get("signature").getAsString()); + String token = authFinishResponseJsonObject.get("token").getAsString(); + + // Validate provided signature against calculated signature + if (!java.util.Arrays.equals(serverSignature, signature)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_AUTHENTICATION); + return; + } + + // Calculate protocol key + SecretKeySpec signingKey = new SecretKeySpec(storedKey, HMAC_SHA256_ALGORITHM); + Mac mac; + byte[] protocolKeyHMAC; + try { + mac = Mac.getInstance(HMAC_SHA256_ALGORITHM); + mac.init(signingKey); + mac.update("Session Key".getBytes()); + mac.update(authMessage.getBytes()); + mac.update(clientKey); + protocolKeyHMAC = mac.doFinal(); + } catch (NoSuchAlgorithmException | InvalidKeyException e1) { + // Since the necessary libraries are provided, this should not happen + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + COMMUNICATION_ERROR_AUTHENTICATION); + return; + } + + byte[] data; + byte[] iv; + + // AES GCM stuff + iv = new byte[16]; + + new SecureRandom().nextBytes(iv); + + SecretKeySpec skeySpec = new SecretKeySpec(protocolKeyHMAC, "AES"); + GCMParameterSpec param = new GCMParameterSpec(protocolKeyHMAC.length * 8 - AES_GCM_TAG_LENGTH, iv); + + Cipher cipher; + try { + cipher = Cipher.getInstance("AES_256/GCM/NOPADDING"); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec, param); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException e1) { + // The java installation does not support AES encryption in GCM mode + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_AUTHENTICATION); + return; + } + try { + data = cipher.doFinal(token.getBytes("UTF-8")); + } catch (IllegalBlockSizeException | BadPaddingException | UnsupportedEncodingException e1) { + // No JSON answer received + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, COMMUNICATION_ERROR_JSON); + return; + } + + byte[] ciphertext = new byte[data.length - AES_GCM_TAG_LENGTH / 8]; + byte[] gcmTag = new byte[AES_GCM_TAG_LENGTH / 8]; + System.arraycopy(data, 0, ciphertext, 0, data.length - AES_GCM_TAG_LENGTH / 8); + System.arraycopy(data, data.length - AES_GCM_TAG_LENGTH / 8, gcmTag, 0, AES_GCM_TAG_LENGTH / 8); + + JsonObject createSessionJsonObject = new JsonObject(); + createSessionJsonObject.addProperty("transactionId", transactionId); + createSessionJsonObject.addProperty("iv", Base64.getEncoder().encodeToString(iv)); + createSessionJsonObject.addProperty("tag", Base64.getEncoder().encodeToString(gcmTag)); + createSessionJsonObject.addProperty("payload", Base64.getEncoder().encodeToString(ciphertext)); + + // finally create the session for further communication + ContentResponse createSessionResponseContentResponse; + JsonObject createSessionResponseJsonObject; + try { + createSessionResponseContentResponse = ThirdGenerationHttpHelper.executeHttpPost(httpClient, config.url, + AUTH_CREATE_SESSION, createSessionJsonObject); + createSessionResponseJsonObject = ThirdGenerationHttpHelper + .getJsonObjectFromResponse(createSessionResponseContentResponse); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + COMMUNICATION_ERROR_AUTHENTICATION); + return; + } + // 200 is the desired status code + if (createSessionResponseContentResponse.getStatus() == 400) { + // Authentication failed + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + CONFIGURATION_ERROR_PASSWORD); + return; + } + + sessionId = createSessionResponseJsonObject.get("sessionId").getAsString(); + + updateStatus(ThingStatus.ONLINE); + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHttpHelper.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHttpHelper.java new file mode 100644 index 0000000000..9759b6f58d --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHttpHelper.java @@ -0,0 +1,157 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link ThirdGenerationHttpHelper} is handling http communication with the device + * handlers. + * + * @author René Stakemeier - Initial contribution + */ +final class ThirdGenerationHttpHelper { + + private ThirdGenerationHttpHelper() { + } + + // base URL of the web api + private static final String WEB_API = "/api/v1"; + // GSON handler + private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + /** + * Helper function to execute a HTTP post request + * + * @param httpClient httpClient to use for communication + * @param url IP or hostname or the device + * @param resource web API resource to post to + * @param parameters the JSON content to post + * @return the HTTP response for the created post request + * @throws ExecutionException Error during the execution of the http request + * @throws TimeoutException Connection timed out + * @throws InterruptedException Connection interrupted + */ + static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonObject parameters) + throws InterruptedException, TimeoutException, ExecutionException { + return executeHttpPost(httpClient, url, resource, parameters, null); + } + + /** + * Helper function to execute a HTTP post request + * + * @param httpClient httpClient to use for communication + * @param url IP or hostname or the device + * @param resource web API resource to post to + * @param sessionId optional session ID + * @param parameters the JSON content to post + * @return the HTTP response for the created post request + * @throws ExecutionException Error during the execution of the http request + * @throws TimeoutException Connection timed out + * @throws InterruptedException Connection interrupted + */ + static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonElement parameters, + @Nullable String sessionId) throws InterruptedException, TimeoutException, ExecutionException { + Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http") + .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.POST) + .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json") + .timeout(5, TimeUnit.SECONDS); + response.content(new StringContentProvider(parameters.toString())); + if (sessionId != null) { + response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId)); + } + return response.send(); + } + + /** + * Helper function to execute a HTTP get request + * + * @param httpClient httpClient to use for communication + * @param url IP or hostname or the device + * @param resource web API resource to get + * @return the HTTP response for the created get request + * @throws ExecutionException Error during the execution of the http request + * @throws TimeoutException Connection timed out + * @throws InterruptedException Connection interrupted + */ + static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource) + throws InterruptedException, TimeoutException, ExecutionException { + return executeHttpGet(httpClient, url, resource, null); + } + + /** + * Helper function to execute a HTTP get request + * + * @param httpClient httpClient to use for communication + * @param url IP or hostname or the device + * @param resource web API resource to get + * @param sessionId optional session ID + * @return the HTTP response for the created get request + * @throws ExecutionException Error during the execution of the http request + * @throws TimeoutException Connection timed out + * @throws InterruptedException Connection interrupted + * @throws Exception thrown if there are communication problems + */ + static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource, + @Nullable String sessionId) throws InterruptedException, TimeoutException, ExecutionException { + Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http") + .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.GET) + .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json") + .timeout(5, TimeUnit.SECONDS); + if (sessionId != null) { + response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId)); + } + return response.send(); + } + + /** + * Helper to extract the JsonArray from a HTTP response. + * Use only, if you expect a JsonArray and no other types (e.g. JSON array)! + * + * @param reponse the HTTP response + * @return the JSON object + */ + static JsonArray getJsonArrayFromResponse(ContentResponse reponse) { + return GSON.fromJson(reponse.getContentAsString(), JsonArray.class); + } + + /** + * Helper to extract the JSON object from a HTTP response. + * Use only, if you expect a JSON object and no other types (e.g. JSON array)! + * + * @param reponse the HTTP response + * @return the JSON object + */ + static JsonObject getJsonObjectFromResponse(ContentResponse reponse) { + return GSON.fromJson(reponse.getContentAsString(), JsonObject.class); + } +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationInverterTypes.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationInverterTypes.java new file mode 100644 index 0000000000..f49b03d34c --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationInverterTypes.java @@ -0,0 +1,37 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +/** + * The {@link ThirdGenerationInverterTypes} contains the list of supported devices + * Each one is represented by a dedicated thing + * + * @author René Stakemeier - Initial contribution + */ +public enum ThirdGenerationInverterTypes { + PLENTICORE_PLUS_42_WITH_BATTERY, + PLENTICORE_PLUS_42_WITHOUT_BATTERY, + PLENTICORE_PLUS_55_WITH_BATTERY, + PLENTICORE_PLUS_55_WITHOUT_BATTERY, + PLENTICORE_PLUS_70_WITH_BATTERY, + PLENTICORE_PLUS_70_WITHOUT_BATTERY, + PLENTICORE_PLUS_85_WITH_BATTERY, + PLENTICORE_PLUS_85_WITHOUT_BATTERY, + PLENTICORE_PLUS_100_WITH_BATTERY, + PLENTICORE_PLUS_100_WITHOUT_BATTERY, + PIKOIQ_42, + PIKOIQ_55, + PIKOIQ_70, + PIKOIQ_85, + PIKOIQ_100 +} diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java new file mode 100644 index 0000000000..3d61b8a39a --- /dev/null +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java @@ -0,0 +1,249 @@ +/** + * 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.kostalinverter.internal.thirdgeneration; + +import static org.openhab.binding.kostalinverter.internal.thirdgeneration.ThirdGenerationBindingConstants.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The {@link ThirdGenerationMappingInverterToChannel} is responsible for the management of the channels + * available to inverters + * + * @author René Stakemeier - Initial contribution + */ +class ThirdGenerationMappingInverterToChannel { + + private static final Map> CHANNEL_MAPPING = new HashMap<>(); + + /* + * Assign the channels to the devices. + */ + static { + List allInvertersList = Arrays.asList(ThirdGenerationInverterTypes.values()); + List allBatteryInvertersList = Stream + .of(ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITH_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITH_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITH_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITH_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITH_BATTERY) + .collect(Collectors.toList()); + List allInvertersWithThreeStringsList = Stream + .of(ThirdGenerationInverterTypes.PLENTICORE_PLUS_42_WITHOUT_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_55_WITHOUT_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_70_WITHOUT_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_85_WITHOUT_BATTERY, + ThirdGenerationInverterTypes.PLENTICORE_PLUS_100_WITHOUT_BATTERY) + .collect(Collectors.toList()); + + // Channels available on all devices + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_DC_POWER, "devices:local", "Dc_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_GRID, "devices:local", + "HomeGrid_P", ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_OWNCONSUMPTION, "devices:local", "HomeOwn_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_PV, "devices:local", "HomePv_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_TOTAL, "devices:local", "Home_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_LIMIT_EVU_ABSOLUTE, "devices:local", "LimitEvuAbs", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_LIMIT_EVU_RELATIV, "devices:local", "LimitEvuRel", + ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_WORKTIME, "devices:local", "WorkTime", + ThirdGenerationChannelDatatypes.SECONDS); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE, "devices:local:ac", + "L1_I", ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER, "devices:local:ac", "L1_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_VOLTAGE, "devices:local:ac", + "L1_U", ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_AMPERAGE, "devices:local:ac", + "L2_I", ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_POWER, "devices:local:ac", "L2_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_2_CURRENT_VOLTAGE, "devices:local:ac", + "L2_U", ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_AMPERAGE, "devices:local:ac", + "L3_I", ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_POWER, "devices:local:ac", "L3_P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_3_CURRENT_VOLTAGE, "devices:local:ac", + "L3_U", ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_CURRENT_POWER, "devices:local:ac", "P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_AMPERAGE, "devices:local:pv1", "I", + ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_POWER, "devices:local:pv1", "P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_1_VOLTAGE, "devices:local:pv1", "U", + ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_AMPERAGE, "devices:local:pv2", "I", + ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_POWER, "devices:local:pv2", "P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_PVSTRING_2_VOLTAGE, "devices:local:pv2", "U", + ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_MC, "scb:event", "ErrMc", + ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_SFH, "scb:event", "ErrSFH", + ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_ERROR_COUNT_SCB, "scb:event", "Event:ActiveErrorCnt", + ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allInvertersList, CHANNEL_SCB_EVENT_WARNING_COUNT_SCB, "scb:event", "Event:ActiveWarningCnt", + ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_DAY, "scb:statistic:EnergyFlow", + "Statistic:Autarky:Day", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_MONTH, "scb:statistic:EnergyFlow", + "Statistic:Autarky:Month", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_TOTAL, "scb:statistic:EnergyFlow", + "Statistic:Autarky:Total", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_AUTARKY_YEAR, "scb:statistic:EnergyFlow", + "Statistic:Autarky:Year", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_DAY, "scb:statistic:EnergyFlow", + "Statistic:CO2Saving:Day", ThirdGenerationChannelDatatypes.KILOGRAM); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_MONTH, "scb:statistic:EnergyFlow", + "Statistic:CO2Saving:Month", ThirdGenerationChannelDatatypes.KILOGRAM); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_TOTAL, "scb:statistic:EnergyFlow", + "Statistic:CO2Saving:Total", ThirdGenerationChannelDatatypes.KILOGRAM); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_CO2SAVING_YEAR, "scb:statistic:EnergyFlow", + "Statistic:CO2Saving:Year", ThirdGenerationChannelDatatypes.KILOGRAM); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_DAY, "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_MONTH, "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Month", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_TOTAL, "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Total", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_YEAR, "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_DAY, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Day", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_MONTH, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Month", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_TOTAL, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Total", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_GRID_YEAR, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeGrid:Year", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_DAY, "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_MONTH, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomePv:Month", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_TOTAL, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomePv:Total", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_PV_YEAR, "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_DAY, "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Day", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_MONTH, "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Month", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_TOTAL, "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Total", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_OWNCONSUMPTION_RATE_YEAR, "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Year", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_DAY, "scb:statistic:EnergyFlow", + "Statistic:Yield:Day", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_MONTH, "scb:statistic:EnergyFlow", + "Statistic:Yield:Month", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_TOTAL, "scb:statistic:EnergyFlow", + "Statistic:Yield:Total", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allInvertersList, CHANNEL_STATISTIC_YIELD_YEAR, "scb:statistic:EnergyFlow", + "Statistic:Yield:Year", ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + + // Plenticore Plus devices can be expanded with a battery. + // Additional channels become available, but the pv3 information are hidden (since the battery is attached + // there) + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_LOADING_CYCLES, + "devices:local:battery", "Cycles", ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_FULL_CHARGE_CAPACITY, + "devices:local:battery", "FullChargeCap_E", ThirdGenerationChannelDatatypes.AMPERE_HOUR); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_AMPERAGE, "devices:local:battery", "I", + ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_POWER, "devices:local:battery", "P", + ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_STATE_OF_CHARGE, + "devices:local:battery", "SoC", ThirdGenerationChannelDatatypes.PERCEMTAGE); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_BATTERY_VOLTAGE, "devices:local:battery", "U", + ThirdGenerationChannelDatatypes.VOLT); + addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_DAY, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Day", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_MONTH, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Month", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_TOTAL, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Total", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allBatteryInvertersList, CHANNEL_STATISTIC_HOMECONSUMPTION_FROM_BATTERIE_YEAR, + "scb:statistic:EnergyFlow", "Statistic:EnergyHomeBat:Year", + ThirdGenerationChannelDatatypes.KILOWATT_HOUR); + addInverterChannel(allBatteryInvertersList, CHANNEL_DEVICE_LOCAL_HOMECONSUMPTION_FROM_BATTERY, "devices:local", + "HomeBat_P", ThirdGenerationChannelDatatypes.WATT); + // Plenticore devices without battery have the pv3 settings available + addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_AMPERAGE, + "devices:local:pv3", "I", ThirdGenerationChannelDatatypes.AMPERE); + addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_POWER, "devices:local:pv3", + "P", ThirdGenerationChannelDatatypes.WATT); + addInverterChannel(allInvertersWithThreeStringsList, CHANNEL_DEVICE_LOCAL_PVSTRING_3_VOLTAGE, + "devices:local:pv3", "U", ThirdGenerationChannelDatatypes.VOLT); + } + + static Map> getModuleToChannelsMappingForInverter( + ThirdGenerationInverterTypes inverter) { + Map> results = new HashMap<>(); + for (ThirdGenerationChannelMappingToWebApi mapping : CHANNEL_MAPPING.get(inverter)) { + List channelList = null; + if (results.containsKey(mapping.moduleId)) { + channelList = results.get(mapping.moduleId); + } else { + channelList = new ArrayList<>(); + results.put(mapping.moduleId, channelList); + } + channelList.add(mapping); + } + return results; + } + + private static void addInverterChannel(ThirdGenerationInverterTypes inverter, + ThirdGenerationChannelMappingToWebApi mapping) { + if (!CHANNEL_MAPPING.containsKey(inverter)) { + CHANNEL_MAPPING.put(inverter, new ArrayList<>()); + } + CHANNEL_MAPPING.get(inverter).add(mapping); + } + + private static void addInverterChannel(ThirdGenerationInverterTypes inverter, String channelUID, String moduleId, + String processdataId, ThirdGenerationChannelDatatypes dataType) { + addInverterChannel(inverter, + new ThirdGenerationChannelMappingToWebApi(channelUID, moduleId, processdataId, dataType)); + } + + private static void addInverterChannel(List inverterList, String channelUID, + String moduleId, String processdataId, ThirdGenerationChannelDatatypes dataType) { + for (ThirdGenerationInverterTypes inverter : inverterList) { + addInverterChannel(inverter, channelUID, moduleId, processdataId, dataType); + } + } +}