diff --git a/bundles/org.openhab.binding.generacmobilelink/README.md b/bundles/org.openhab.binding.generacmobilelink/README.md index 4b42a9517..a450b3987 100644 --- a/bundles/org.openhab.binding.generacmobilelink/README.md +++ b/bundles/org.openhab.binding.generacmobilelink/README.md @@ -36,22 +36,25 @@ The MobileLink account bridge must be added manually. Once added, generator thin All channels are read-only. -| channel | type | description | -|-------------------------|----------------------|-------------------------------------------| -| connected | Switch | Connected status | -| greenLight | Switch | Green light state (typically auto mode) | -| yellowLight | Switch | Yellow light state | -| redLight | Switch | Red light state (typically off mode) | -| blueLight | Switch | Blue light state (typically running mode) | -| statusDate | DateTime | Status date (start of day) | -| status | String | General status | -| currentAlarmDescription | String | Current alarm description | -| runHours | Number:Time | Number of run hours | -| exerciseHours | Number:Time | Number of exercise hours | -| fuelType | Number | Fuel type | -| fuelLevel | Number:Dimensionless | Fuel level | -| batteryVoltage | String | Battery voltage status | -| serviceStatus | Switch | Service status | +| Channel ID | Item Type | Description | +|----------------------|-----------------------------|-----------------------------------| +| heroImageUrl | String | Hero Image URL | +| statusLabel | String | Status Label | +| statusText | String | Status Text | +| activationDate | DateTime | Activation Date | +| deviceSsid | String | Device SSID | +| status | Number | Status | +| isConnected | Switch | Is Connected | +| isConnecting | Switch | Is Connecting | +| showWarning | Switch | Show Warning | +| hasMaintenanceAlert | Switch | Has Maintenance Alert | +| lastSeen | DateTime | Last Seen | +| connectionTime | DateTime | Connection Time | +| runHours | Number:Time | Number of Hours Run | +| batteryVoltage | Number:ElectricPotential | Battery Voltage | +| hoursOfProtection | Number:Time | Number of Hours of Protection | +| signalStrength | Number:Dimensionless | Signal Strength | + ## Full Example @@ -66,27 +69,41 @@ Bridge generacmobilelink:account:main "MobileLink Account" [ userName="foo@bar.c ### Items ```java -Switch GeneratorConnected "Connected [%s]" {channel="generacmobilelink:generator:main:123456:connected"} -Switch GeneratorGreenLight "Green Light [%s]" {channel="generacmobilelink:generator:main:123456:greenLight"} -Switch GeneratorYellowLight "Yellow Light [%s]" {channel="generacmobilelink:generator:main:123456:yellowLight"} -Switch GeneratorBlueLight "Blue Light [%s]" {channel="generacmobilelink:generator:main:123456:blueLight"} -Switch GeneratorRedLight "Red Light [%s]" {channel="generacmobilelink:generator:main:123456:redLight"} -String GeneratorStatus "Status [%s]" {channel="generacmobilelink:generator:main:123456:status"} -String GeneratorAlarm "Alarm [%s]" {channel="generacmobilelink:generator:main:123456:currentAlarmDescription"} +String GeneratorHeroImageUrl "Hero Image URL [%s]" { channel="generacmobilelink:generator:main:123456:heroImageUrl" } +String GeneratorStatusLabel "Status Label [%s]" { channel="generacmobilelink:generator:main:123456:statusLabel" } +String GeneratorStatusText "Status Text [%s]" { channel="generacmobilelink:generator:main:123456:statusText" } +DateTime GeneratorActivationDate "Activation Date [%s]" { channel="generacmobilelink:generator:main:123456:activationDate" } +String GeneratorDeviceSsid "Device SSID [%s]" { channel="generacmobilelink:generator:main:123456:deviceSsid" } +Number GeneratorStatus "Status [%d]" { channel="generacmobilelink:generator:main:123456:status" } +Switch GeneratorIsConnected "Is Connected [%s]" { channel="generacmobilelink:generator:main:123456:isConnected" } +Switch GeneratorIsConnecting "Is Connecting [%s]" { channel="generacmobilelink:generator:main:123456:isConnecting" } +Switch GeneratorShowWarning "Show Warning [%s]" { channel="generacmobilelink:generator:main:123456:showWarning" } +Switch GeneratorHasMaintenanceAlert "Has Maintenance Alert [%s]" { channel="generacmobilelink:generator:main:123456:hasMaintenanceAlert" } +DateTime GeneratorLastSeen "Last Seen [%s]" { channel="generacmobilelink:generator:main:123456:lastSeen" } +DateTime GeneratorConnectionTime "Connection Time [%s]" { channel="generacmobilelink:generator:main:123456:connectionTime" } +Number:Time GeneratorRunHours "Number of Hours Run [%d]" { channel="generacmobilelink:generator:main:123456:runHours" } +Number:ElectricPotential GeneratorBatteryVoltage "Battery Voltage [%d]v" { channel="generacmobilelink:generator:main:123456:batteryVoltage" } +Number:Time GeneratorHoursOfProtection "Number of Hours of Protection [%d]" { channel="generacmobilelink:generator:main:123456:hoursOfProtection" } +Number:Dimensionless GeneratorSignalStrength "Signal Strength [%d]" { channel="generacmobilelink:generator:main:123456:signalStrength" } + ``` ### Sitemap ```perl -sitemap MobileLink label="Demo Sitemap" { - Frame label="Generator" { - Switch item=GeneratorConnected - Switch item=GeneratorGreenLight - Switch item=GeneratorYellowLight - Switch item=GeneratorBlueLight - Switch item=GeneratorRedLight - Text item=GeneratorStatus - Text item=GeneratorAlarm - } +sitemap generacmobilelink label="Generac MobileLink" +{ + Frame label="Generator Status" { + Text item=GeneratorStatus + Text item=GeneratorStatusLabel + Text item=GeneratorStatusText + } + + Frame label="Generator Properties" { + Text item=GeneratorRunHours + Text item=GeneratorHoursOfProtection + Text item=GeneratorBatteryVoltage + Text item=GeneratorSignalStrength + } } ``` diff --git a/bundles/org.openhab.binding.generacmobilelink/pom.xml b/bundles/org.openhab.binding.generacmobilelink/pom.xml index 36c7c4722..30d73c0cc 100644 --- a/bundles/org.openhab.binding.generacmobilelink/pom.xml +++ b/bundles/org.openhab.binding.generacmobilelink/pom.xml @@ -14,4 +14,12 @@ openHAB Add-ons :: Bundles :: GeneracMobileLink Binding + + + org.jsoup + jsoup + 1.14.3 + provided + + diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml b/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml index f569b0477..f06136a2a 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml @@ -4,6 +4,7 @@ openhab-runtime-base + mvn:org.jsoup/jsoup/1.14.3 mvn:org.openhab.addons.bundles/org.openhab.binding.generacmobilelink/${project.version} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java index 86542ea1d..bf73de281 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java @@ -23,7 +23,26 @@ import org.openhab.core.thing.ThingTypeUID; */ @NonNullByDefault public class GeneracMobileLinkBindingConstants { - private static final String BINDING_ID = "generacmobilelink"; + public static final String BINDING_ID = "generacmobilelink"; public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); public static final ThingTypeUID THING_TYPE_GENERATOR = new ThingTypeUID(BINDING_ID, "generator"); + + public static final String PROPERTY_GENERATOR_ID = "generatorId"; + + public static final String CHANNEL_HERO_IMAGE_URL = "heroImageUrl"; + public static final String CHANNEL_STATUS_LABEL = "statusLabel"; + public static final String CHANNEL_STATUS_TEXT = "statusText"; + public static final String CHANNEL_ACTIVATION_DATE = "activationDate"; + public static final String CHANNEL_DEVICE_SSID = "deviceSsid"; + public static final String CHANNEL_STATUS = "status"; + public static final String CHANNEL_IS_CONNECTED = "isConnected"; + public static final String CHANNEL_IS_CONNECTING = "isConnecting"; + public static final String CHANNEL_SHOW_WARNING = "showWarning"; + public static final String CHANNEL_HAS_MAINTENANCE_ALERT = "hasMaintenanceAlert"; + public static final String CHANNEL_LAST_SEEN = "lastSeen"; + public static final String CHANNEL_CONNECTION_TIME = "connectionTime"; + public static final String CHANNEL_RUN_HOURS = "runHours"; + public static final String CHANNEL_BATTERY_VOLTAGE = "batteryVoltage"; + public static final String CHANNEL_HOURS_OF_PROTECTION = "hoursOfProtection"; + public static final String CHANNEL_SIGNAL_STRENGH = "signalStrength"; } diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkGeneratorConfiguration.java similarity index 56% rename from bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java rename to bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkGeneratorConfiguration.java index da7210880..a11a5b2c0 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkGeneratorConfiguration.java @@ -10,16 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.generacmobilelink.internal.dto; +package org.openhab.binding.generacmobilelink.internal.config; -import java.util.ArrayList; +import org.eclipse.jdt.annotation.NonNullByDefault; /** - * {@link GeneratorStatusResponseDTO} response from the MobileLink API + * The {@link GeneracMobileLinkGeneratorConfiguration} class contains fields mapping thing configuration parameters. * * @author Dan Cunningham - Initial contribution */ -@SuppressWarnings("serial") -public class GeneratorStatusResponseDTO extends ArrayList { +@NonNullByDefault +public class GeneracMobileLinkGeneratorConfiguration { + public String generatorId = ""; } diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java index f6b8fb7cf..78c9c5567 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java @@ -12,21 +12,21 @@ */ package org.openhab.binding.generacmobilelink.internal.discovery; -import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR; +import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants; -import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO; +import org.openhab.binding.generacmobilelink.internal.dto.Apparatus; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; /** - * The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering generator things + * The {@link GeneracMobileLinkDiscoveryService} is responsible for discovering device things * * @author Dan Cunningham - Initial contribution */ @@ -52,13 +52,13 @@ public class GeneracMobileLinkDiscoveryService extends AbstractDiscoveryService return false; } - public void generatorDiscovered(GeneratorStatusDTO generator, ThingUID bridgeUID) { + public void generatorDiscovered(Apparatus apparatus, ThingUID bridgeUID) { DiscoveryResult result = DiscoveryResultBuilder - .create(new ThingUID(GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR, bridgeUID, - String.valueOf(generator.gensetID))) - .withLabel("MobileLink Generator " + generator.generatorName) - .withProperty("generatorId", String.valueOf(generator.gensetID)) - .withRepresentationProperty("generatorId").withBridge(bridgeUID).build(); + .create(new ThingUID(THING_TYPE_GENERATOR, bridgeUID, String.valueOf(apparatus.apparatusId))) + .withLabel("MobileLink Generator " + apparatus.name) + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, String.valueOf(apparatus.serialNumber)) + .withProperty(PROPERTY_GENERATOR_ID, String.valueOf(apparatus.apparatusId)) + .withRepresentationProperty(PROPERTY_GENERATOR_ID).withBridge(bridgeUID).build(); thingDiscovered(result); } } diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Account.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Account.java new file mode 100644 index 000000000..a3b520e3e --- /dev/null +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Account.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; + +/** + * The {@link Account} represents a Generac Account + * + * @author Dan Cunningham - Initial contribution + */ +public class Account { + public String userId; + public String firstName; + public String lastName; + public String[] emails; + public String[] phoneNumbers; + public String[] groups; + public MobileLinkSettings mobileLinkSettings; + + public class MobileLinkSettings { + public DisplaySettings displaySettings; + + public class DisplaySettings { + public String distanceUom; + public String temperatureUom; + } + } +} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Apparatus.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Apparatus.java new file mode 100644 index 000000000..109b6b3b5 --- /dev/null +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Apparatus.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; + +import java.util.List; + +/** + * The {@link Apparatus} represents a Generac Apparatus (Generator) + * + * @author Dan Cunningham - Initial contribution + */ +public class Apparatus { + public int apparatusId; + public String serialNumber; + public String name; + public int type; + public String localizedAddress; + public String materialDescription; + public String heroImageUrl; + public int apparatusStatus; + public boolean isConnected; + public boolean isConnecting; + public boolean showWarning; + public Weather weather; + public String preferredDealerName; + public String preferredDealerPhone; + public String preferredDealerEmail; + public boolean isDealerManaged; + public boolean isDealerUnmonitored; + public String modelNumber; + public String panelId; + public List properties; + + public class Weather { + public Temperature temperature; + public int iconCode; + + public class Temperature { + public double value; + public String unit; + public int unitType; + } + } + + public class Property { + public String name; + public Value value; + public int type; + + public class Value { + public int type; + public String status; + public boolean isLegacy; + public boolean isDunning; + public String deviceId; + public String deviceType; + public String signalStrength; + public String batteryLevel; + } + } + + public class Device { + public String deviceId; + public String deviceType; + public String signalStrength; + public String batteryLevel; + public String status; + } +} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusDetail.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusDetail.java new file mode 100644 index 000000000..2d0f5cd41 --- /dev/null +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusDetail.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; + +import java.time.ZonedDateTime; + +/** + * The {@link ApparatusDetail} represents the details of a Generac Apparatus + * + * @author Dan Cunningham - Initial contribution + */ +public class ApparatusDetail { + public int apparatusId; + public String name; + public String serialNumber; + public int apparatusClassification; + public String panelId; + public ZonedDateTime activationDate; + public String deviceType; + public String deviceSsid; + public String shortDeviceId; + public int apparatusStatus; + public String heroImageUrl; + public String statusLabel; + public String statusText; + public String eCodeLabel; + public Weather weather; + public boolean isConnected; + public boolean isConnecting; + public boolean showWarning; + public boolean hasMaintenanceAlert; + public ZonedDateTime lastSeen; + public String connectionTimestamp; + public Address address; + public Property[] properties; + public Subscription subscription; + public boolean enrolledInVpp; + public boolean hasActiveVppEvent; + public ProductInfo[] productInfo; + public boolean hasDisconnectedNotificationsOn; + + public class Weather { + public Temperature temperature; + public int iconCode; + + public class Temperature { + public double value; + public String unit; + public int unitType; + } + } + + public class Address { + public String line1; + public String line2; + public String city; + public String region; + public String country; + public String postalCode; + } + + public class Property { + public String name; + public String value; + public int type; + } + + public class Subscription { + public int type; + public int status; + public boolean isLegacy; + public boolean isDunning; + } + + public class ProductInfo { + public String name; + public String value; + public int type; + } +} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusInfo.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusInfo.java new file mode 100644 index 000000000..9d48a64f4 --- /dev/null +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusInfo.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; + +/** + * The {@link ApparatusInfo} represents the info of a Generac Apparatus + * + * @author Dan Cunningham - Initial contribution + */ +public class ApparatusInfo { + public int apparatusId; + public String apparatusName; + public String productType; + public String description; + public Property[] properties; + public Attribute[] attributes; + + public class Property { + public String name; + public String value; + public int type; + } + + public class Attribute { + public String name; + public String value; + public int type; + } +} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java deleted file mode 100644 index 976be8762..000000000 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; - -/** - * {@link GeneratorStatusDTO} object from the MobileLink API - * - * @author Dan Cunningham - Initial contribution - */ -public class GeneratorStatusDTO { - public Integer gensetID; - public String generatorDate; - public String generatorName; - public String generatorSerialNumber; - public String generatorModel; - public String generatorDescription; - public String generatorMDN; - public String generatorImei; - public String generatorIccid; - public String generatorTetherSerial; - public Boolean connected; - public Boolean greenLightLit; - public Boolean yellowLightLit; - public Boolean redLightLit; - public Boolean blueLightLit; - public String generatorStatus; - public String generatorStatusDate; - public String currentAlarmDescription; - public Integer runHours; - public Integer exerciseHours; - public String batteryVoltage; - public Integer fuelType; - public Integer fuelLevel; - public String generatorBrandImageURL; - public Boolean generatorServiceStatus; - public String signalStrength; - public String deviceId; - public Integer deviceTypeId; - public String firmwareVersion; - public String timezone; - public String mACAddress; - public String iPAddress; - public String sSID; -} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java deleted file mode 100644 index fbba4afb3..000000000 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; - -/** - * {@link LoginRequestDTO} request for the MobileLink API - * - * @author Dan Cunningham - Initial contribution - */ -public class LoginRequestDTO { - public LoginRequestDTO(String sharedKey, String userLogin, String userPassword) { - super(); - this.sharedKey = sharedKey; - this.userLogin = userLogin; - this.userPassword = userPassword; - } - - public String sharedKey; - public String userLogin; - public String userPassword; -} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java deleted file mode 100644 index 401e76c56..000000000 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; - -/** - * {@link LoginResponseDTO} response from the MobileLink API - * - * @author Dan Cunningham - Initial contribution - */ -public class LoginResponseDTO { - public String authToken; - public String pushChannelName; -} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SelfAssertedResponse.java similarity index 70% rename from bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java rename to bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SelfAssertedResponse.java index c45de609d..6a895ad66 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SelfAssertedResponse.java @@ -13,11 +13,12 @@ package org.openhab.binding.generacmobilelink.internal.dto; /** - * {@link ErrorResponseDTO} object from the MobileLink API + * The {@link SelfAssertedResponse} represents the SelfAssertedResponse object used in login * * @author Dan Cunningham - Initial contribution */ -public class ErrorResponseDTO { - public Integer errorCode; - public String errorMessage; +public class SelfAssertedResponse { + public String status; + public String errorCode; + public String message; } diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SignInConfig.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SignInConfig.java new file mode 100644 index 000000000..87a650016 --- /dev/null +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SignInConfig.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2023 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.generacmobilelink.internal.dto; + +import java.util.Map; + +/** + * /** + * The {@link SignInConfig} represents the SignInConfig object used in login + * + * @author Dan Cunningham - Initial contribution + */ +public class SignInConfig { + public String remoteResource; + public int retryLimit; + public boolean trimSpacesInPassword; + public String api; + public String csrf; + public String transId; + public String pageViewId; + public boolean suppressElementCss; + public boolean isPageViewIdSentWithHeader; + public boolean allowAutoFocusOnPasswordField; + public int pageMode; + public Map config; + public Map hosts; + public Locale locale; + public XhrSettings xhrSettings; + + public class Locale { + public String lang; + } + + public class XhrSettings { + public boolean retryEnabled; + public int retryMaxAttempts; + public int retryDelay; + public int retryExponent; + public String[] retryOn; + } +} diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java index e14371827..2faf4e1f4 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java @@ -21,7 +21,6 @@ import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService; import org.openhab.binding.generacmobilelink.internal.handler.GeneracMobileLinkAccountHandler; import org.openhab.binding.generacmobilelink.internal.handler.GeneracMobileLinkGeneratorHandler; @@ -51,11 +50,11 @@ public class GeneracMobileLinkHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_GENERATOR); private final Map> discoveryServiceRegs = new ConcurrentHashMap<>(); - private final HttpClient httpClient; + private final HttpClientFactory httpClientFactory; @Activate public GeneracMobileLinkHandlerFactory(final @Reference HttpClientFactory httpClientFactory) { - this.httpClient = httpClientFactory.getCommonHttpClient(); + this.httpClientFactory = httpClientFactory; } @Override @@ -74,7 +73,7 @@ public class GeneracMobileLinkHandlerFactory extends BaseThingHandlerFactory { if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) { GeneracMobileLinkDiscoveryService discoveryService = new GeneracMobileLinkDiscoveryService(); GeneracMobileLinkAccountHandler accountHandler = new GeneracMobileLinkAccountHandler((Bridge) thing, - httpClient, discoveryService); + httpClientFactory, discoveryService); discoveryServiceRegs.put(accountHandler.getThing().getUID(), bundleContext .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); return accountHandler; diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java index 864c103d6..a18c0a218 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java @@ -12,36 +12,42 @@ */ package org.openhab.binding.generacmobilelink.internal.handler; +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.client.util.BufferingResponseListener; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.client.util.FormContentProvider; +import org.eclipse.jetty.util.Fields; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; import org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants; import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkAccountConfiguration; +import org.openhab.binding.generacmobilelink.internal.config.GeneracMobileLinkGeneratorConfiguration; import org.openhab.binding.generacmobilelink.internal.discovery.GeneracMobileLinkDiscoveryService; -import org.openhab.binding.generacmobilelink.internal.dto.ErrorResponseDTO; -import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO; -import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusResponseDTO; -import org.openhab.binding.generacmobilelink.internal.dto.LoginRequestDTO; -import org.openhab.binding.generacmobilelink.internal.dto.LoginResponseDTO; +import org.openhab.binding.generacmobilelink.internal.dto.Apparatus; +import org.openhab.binding.generacmobilelink.internal.dto.ApparatusDetail; +import org.openhab.binding.generacmobilelink.internal.dto.SelfAssertedResponse; +import org.openhab.binding.generacmobilelink.internal.dto.SignInConfig; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; 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.ThingUID; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; @@ -49,9 +55,10 @@ import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSyntaxException; /** * The {@link GeneracMobileLinkAccountHandler} is responsible for connecting to the MobileLink cloud service and @@ -61,191 +68,327 @@ import com.google.gson.GsonBuilder; */ @NonNullByDefault public class GeneracMobileLinkAccountHandler extends BaseBridgeHandler { - private static final String BASE_URL = "https://api.mobilelinkgen.com"; - private static final String SHARED_KEY = "GeneseeDepot13"; private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkAccountHandler.class); - private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); - private @Nullable Future pollFuture; - private @Nullable String authToken; - private @Nullable GeneratorStatusResponseDTO generators; - private GeneracMobileLinkDiscoveryService discoveryService; - private HttpClient httpClient; - private int refreshIntervalSeconds = 60; - public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClient httpClient, + private static final String API_BASE = "https://app.mobilelinkgen.com/api"; + private static final String LOGIN_BASE = "https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn"; + private static final Pattern SETTINGS_PATTERN = Pattern.compile("^var SETTINGS = (.*);$", Pattern.MULTILINE); + private static final Gson GSON = new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, + (JsonDeserializer) (json, type, jsonDeserializationContext) -> { + return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString()); + }).create(); + private HttpClient httpClient; + private GeneracMobileLinkDiscoveryService discoveryService; + private Map apparatusesCache = new HashMap(); + private int refreshIntervalSeconds = 60; + private boolean loggedIn; + + private @Nullable Future pollFuture; + + public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClientFactory httpClientFactory, GeneracMobileLinkDiscoveryService discoveryService) { super(bridge); - this.httpClient = httpClient; this.discoveryService = discoveryService; + httpClient = httpClientFactory.createHttpClient(GeneracMobileLinkBindingConstants.BINDING_ID); + httpClient.setFollowRedirects(true); + // We have to send a very large amount of cookies which exceeds the default buffer size + httpClient.setRequestBufferSize(16348); + try { + httpClient.start(); + } catch (Exception e) { + throw new IllegalStateException("Error starting custom HttpClient", e); + } } @Override public void initialize() { updateStatus(ThingStatus.UNKNOWN); - authToken = null; - restartPoll(); + stopOrRestartPoll(true); } @Override public void dispose() { - stopPoll(); + stopOrRestartPoll(false); + try { + httpClient.stop(); + } catch (Exception e) { + logger.debug("Could not stop HttpClient", e); + } } @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - updateGeneratorThings(); + try { + updateGeneratorThings(); + } catch (IOException | SessionExpiredException e) { + logger.debug("Could refresh things", e); + } } } @Override public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { - GeneratorStatusResponseDTO generatorsLocal = generators; - if (generatorsLocal != null) { - Optional generatorOpt = generatorsLocal.stream() - .filter(g -> String.valueOf(g.gensetID).equals(childThing.getUID().getId())).findFirst(); - if (generatorOpt.isPresent()) { - ((GeneracMobileLinkGeneratorHandler) childHandler).updateGeneratorStatus(generatorOpt.get()); - } + logger.debug("childHandlerInitialized {}", childThing.getUID()); + String id = childThing.getConfiguration().as(GeneracMobileLinkGeneratorConfiguration.class).generatorId; + Apparatus apparatus = apparatusesCache.get(id); + if (apparatus == null) { + logger.debug("No device for id {}", id); + return; + } + try { + updateGeneratorThing(childHandler, apparatus); + } catch (IOException | SessionExpiredException e) { + logger.debug("Could not initialize child", e); } } - private void stopPoll() { - Future localPollFuture = pollFuture; - if (localPollFuture != null) { - localPollFuture.cancel(true); + private synchronized void stopOrRestartPoll(boolean restart) { + Future pollFuture = this.pollFuture; + if (pollFuture != null) { + pollFuture.cancel(true); + this.pollFuture = null; + } + if (restart) { + this.pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 1, refreshIntervalSeconds, TimeUnit.SECONDS); } - } - - private void restartPoll() { - stopPoll(); - pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshIntervalSeconds, TimeUnit.SECONDS); } private void poll() { try { - if (authToken == null) { - logger.debug("Attempting Login"); + if (!loggedIn) { login(); } - getStatuses(true); - } catch (InterruptedException e) { + loggedIn = true; + updateGeneratorThings(); + } catch (IOException e) { + logger.debug("Could not update devices", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/thing.generacmobilelink.account.offline.communication-error.io-exception"); + } catch (SessionExpiredException e) { + logger.debug("Session expired", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/thing.generacmobilelink.account.offline.communication-error.session-expired"); + loggedIn = false; + } catch (InvalidCredentialsException e) { + logger.debug("Credentials Invalid", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/thing.generacmobilelink.account.offline.configuration-error.invalid-credentials"); + loggedIn = false; + // we don't want to continue polling with bad credentials + stopOrRestartPoll(false); } } - private synchronized void login() throws InterruptedException { - GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class); - refreshIntervalSeconds = config.refreshInterval; - HTTPResult result = sendRequest(BASE_URL + "/Users/login", HttpMethod.POST, null, - new StringContentProvider( - gson.toJson(new LoginRequestDTO(SHARED_KEY, config.username, config.password))), - "application/json"); - if (result.responseCode == HttpStatus.OK_200) { - LoginResponseDTO loginResponse = gson.fromJson(result.content, LoginResponseDTO.class); - if (loginResponse != null) { - authToken = loginResponse.authToken; - updateStatus(ThingStatus.ONLINE); - } - } else { - handleErrorResponse(result); - if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) { - // bad credentials, stop trying to login - stopPoll(); - } - } - } - - private void getStatuses(boolean retry) throws InterruptedException { - if (authToken == null) { + private void updateGeneratorThings() throws IOException, SessionExpiredException { + Apparatus[] apparatuses = getEndpoint(Apparatus[].class, "/v2/Apparatus/list"); + if (apparatuses == null) { + logger.debug("Could not decode apparatuses response"); return; } - HTTPResult result = sendRequest(BASE_URL + "/Generator/GeneratorStatus", HttpMethod.GET, authToken, null, null); - if (result.responseCode == HttpStatus.OK_200) { - generators = gson.fromJson(result.content, GeneratorStatusResponseDTO.class); - updateGeneratorThings(); - if (getThing().getStatus() != ThingStatus.ONLINE) { - updateStatus(ThingStatus.ONLINE); + if (getThing().getStatus() != ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } + for (Apparatus apparatus : apparatuses) { + if (apparatus.type != 0) { + logger.debug("Unknown apparatus type {} {}", apparatus.type, apparatus.name); + continue; } - } else { - if (retry) { - logger.debug("Retrying status request"); - getStatuses(false); + + String id = String.valueOf(apparatus.apparatusId); + apparatusesCache.put(id, apparatus); + + Optional thing = getThing().getThings().stream().filter( + t -> t.getConfiguration().as(GeneracMobileLinkGeneratorConfiguration.class).generatorId.equals(id)) + .findFirst(); + if (!thing.isPresent()) { + discoveryService.generatorDiscovered(apparatus, getThing().getUID()); } else { - handleErrorResponse(result); + ThingHandler handler = thing.get().getHandler(); + if (handler != null) { + updateGeneratorThing(handler, apparatus); + } } } } - private HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token, - @Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException { + private void updateGeneratorThing(ThingHandler handler, Apparatus apparatus) + throws IOException, SessionExpiredException { + ApparatusDetail detail = getEndpoint(ApparatusDetail.class, "/v1/Apparatus/details/" + apparatus.apparatusId); + if (detail != null) { + ((GeneracMobileLinkGeneratorHandler) handler).updateGeneratorStatus(apparatus, detail); + } else { + logger.debug("Could not decode apparatuses detail response"); + } + } + + private @Nullable T getEndpoint(Class clazz, String endpoint) throws IOException, SessionExpiredException { try { - Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS); - if (token != null) { - request = request.header("AuthToken", token); + ContentResponse response = httpClient.newRequest(API_BASE + endpoint).send(); + if (response.getStatus() == 204) { + // no data + return null; } - if (content != null & contentType != null) { - request = request.content(content, contentType); + if (response.getStatus() != 200) { + throw new SessionExpiredException("API returned status code: " + response.getStatus()); } - logger.trace("Sending {} to {}", request.getMethod(), request.getURI()); - final CompletableFuture futureResult = new CompletableFuture<>(); - request.send(new BufferingResponseListener() { - @NonNullByDefault({}) - @Override - public void onComplete(Result result) { - futureResult.complete(new HTTPResult(result.getResponse().getStatus(), getContentAsString())); - } - }); - HTTPResult result = futureResult.get(); - logger.trace("Response - status: {} content: {}", result.responseCode, result.content); - return result; - } catch (ExecutionException e) { - return new HTTPResult(0, e.getMessage()); + String data = response.getContentAsString(); + logger.debug("getEndpoint {}", data); + return GSON.fromJson(data, clazz); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } catch (TimeoutException | ExecutionException | JsonSyntaxException e) { + throw new IOException(e); } } - private void handleErrorResponse(HTTPResult result) { - switch (result.responseCode) { - case HttpStatus.UNAUTHORIZED_401: - // the server responds with a 500 error in some cases when credentials are not correct - case HttpStatus.INTERNAL_SERVER_ERROR_500: - // server returned a valid error response - ErrorResponseDTO error = gson.fromJson(result.content, ErrorResponseDTO.class); - if (error != null && error.errorCode > 0) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Unauthorized: " + result.content); - authToken = null; - break; - } - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, result.content); + /** + * Attempts to login through a Microsoft Azure implicit grant oauth flow + * + * @throws IOException if there is a problem communicating or parsing the responses + * @throws InvalidCredentialsException If Azure rejects the login credentials. + */ + private synchronized void login() throws IOException, InvalidCredentialsException { + logger.debug("Attempting login"); + GeneracMobileLinkAccountConfiguration config = getConfigAs(GeneracMobileLinkAccountConfiguration.class); + refreshIntervalSeconds = config.refreshInterval; + try { + ContentResponse signInResponse = httpClient.newRequest(API_BASE + "/Auth/SignIn?email=" + config.username) + .send(); + + String responseData = signInResponse.getContentAsString(); + logger.trace("response data: {}", responseData); + + // If we are immediately returned a submit form, it means our cookies are still valid with the identity + // provider and we can just try and submit to the API service + if (submitPage(responseData)) { + return; + } + + // Azure wants us to login again, look for the SETTINGS javascript in the page + Matcher matcher = SETTINGS_PATTERN.matcher(responseData); + if (!matcher.find()) { + throw new IOException("Could not find settings string"); + } + + String parseSettings = matcher.group(1); + logger.debug("parseSettings: {}", parseSettings); + SignInConfig signInConfig = GSON.fromJson(parseSettings, SignInConfig.class); + + if (signInConfig == null) { + throw new IOException("Could not parse settings string"); + } + + Fields fields = new Fields(); + fields.put("request_type", "RESPONSE"); + fields.put("signInName", config.username); + fields.put("password", config.password); + + Request selfAssertedRequest = httpClient.POST(LOGIN_BASE + "/SelfAsserted") + .header("X-Csrf-Token", signInConfig.csrf).param("tx", "StateProperties=" + signInConfig.transId) + .param("p", "B2C_1A_SignUpOrSigninOnline").content(new FormContentProvider(fields)); + + ContentResponse selfAssertedResponse = selfAssertedRequest.send(); + + logger.debug("selfAssertedRequest response {}", selfAssertedResponse.getStatus()); + + if (selfAssertedResponse.getStatus() != 200) { + throw new IOException("SelfAsserted: Bad response status: " + selfAssertedResponse.getStatus()); + } + + SelfAssertedResponse sa = GSON.fromJson(selfAssertedResponse.getContentAsString(), + SelfAssertedResponse.class); + + if (sa == null) { + throw new IOException("SelfAsserted Could not parse response JSON"); + } + + if (!"200".equals(sa.status)) { + throw new InvalidCredentialsException("Invalid Credentials: " + sa.message); + } + + Request confirmedRequest = httpClient.newRequest(LOGIN_BASE + "/api/CombinedSigninAndSignup/confirmed") + .param("csrf_token", signInConfig.csrf).param("tx", "StateProperties=" + signInConfig.transId) + .param("p", "B2C_1A_SignUpOrSigninOnline"); + + ContentResponse confirmedResponse = confirmedRequest.send(); + + if (confirmedResponse.getStatus() != 200) { + throw new IOException("CombinedSigninAndSignup bad response: " + confirmedResponse.getStatus()); + } + + String loginString = confirmedResponse.getContentAsString(); + logger.trace("confirmedResponse: {}", loginString); + if (!submitPage(loginString)) { + throw new IOException("Error parsing HTML submit form"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } catch (ExecutionException | TimeoutException | JsonSyntaxException e) { + throw new IOException(e); } } - private void updateGeneratorThings() { - GeneratorStatusResponseDTO generatorsLocal = generators; - if (generatorsLocal != null) { - generatorsLocal.forEach(generator -> { - Thing thing = getThing().getThing(new ThingUID(GeneracMobileLinkBindingConstants.THING_TYPE_GENERATOR, - getThing().getUID(), String.valueOf(generator.gensetID))); - if (thing == null) { - discoveryService.generatorDiscovered(generator, getThing().getUID()); - } else { - ThingHandler handler = thing.getHandler(); - if (handler != null) { - ((GeneracMobileLinkGeneratorHandler) handler).updateGeneratorStatus(generator); - } - } - }); + /** + * Attempts to submit a HTML form from Azure to the Generac API, returns false if the HTML does not match the + * required form + * + * @param loginString + * @return false if the HTML is not a form, true if submission is successful + * @throws ExecutionException + * @throws TimeoutException + * @throws InterruptedException + * @throws JsonSyntaxException + * @throws IOException + */ + private boolean submitPage(String loginString) + throws ExecutionException, TimeoutException, InterruptedException, JsonSyntaxException, IOException { + Document loginPage = Jsoup.parse(loginString); + Element form = loginPage.select("form").first(); + Element loginState = loginPage.select("input[name=state]").first(); + Element loginCode = loginPage.select("input[name=code]").first(); + + if (form == null || loginState == null || loginCode == null) { + logger.debug("Could not load login page"); + return false; + } + + // url that the form will submit to + String action = form.attr("action"); + + Fields fields = new Fields(); + fields.put("state", loginState.attr("value")); + fields.put("code", loginCode.attr("value")); + + Request loginRequest = httpClient.POST(action).content(new FormContentProvider(fields)); + + ContentResponse loginResponse = loginRequest.send(); + if (logger.isTraceEnabled()) { + logger.trace("login response {} {}", loginResponse.getStatus(), loginResponse.getContentAsString()); + } else { + logger.debug("login response status {}", loginResponse.getStatus()); + } + if (loginResponse.getStatus() != 200) { + throw new IOException("Bad api login resposne: " + loginResponse.getStatus()); + } + return true; + } + + private class InvalidCredentialsException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidCredentialsException(String message) { + super(message); } } - public static class HTTPResult { - public @Nullable String content; - public final int responseCode; + private class SessionExpiredException extends Exception { + private static final long serialVersionUID = 1L; - public HTTPResult(int responseCode, @Nullable String content) { - this.responseCode = responseCode; - this.content = content; + public SessionExpiredException(String message) { + super(message); } } } diff --git a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java index ea2bb3858..a02fbf91a 100644 --- a/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java +++ b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java @@ -12,16 +12,18 @@ */ package org.openhab.binding.generacmobilelink.internal.handler; -import java.time.LocalDate; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; +import static org.openhab.binding.generacmobilelink.internal.GeneracMobileLinkBindingConstants.*; +import java.util.Arrays; + +import javax.measure.quantity.Dimensionless; +import javax.measure.quantity.ElectricPotential; import javax.measure.quantity.Time; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.generacmobilelink.internal.dto.GeneratorStatusDTO; +import org.openhab.binding.generacmobilelink.internal.dto.Apparatus; +import org.openhab.binding.generacmobilelink.internal.dto.ApparatusDetail; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -34,6 +36,7 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +48,9 @@ import org.slf4j.LoggerFactory; @NonNullByDefault public class GeneracMobileLinkGeneratorHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(GeneracMobileLinkGeneratorHandler.class); - private @Nullable GeneratorStatusDTO status; + + private @Nullable Apparatus apparatus; + private @Nullable ApparatusDetail apparatusDetail; public GeneracMobileLinkGeneratorHandler(Thing thing) { super(thing); @@ -63,37 +68,66 @@ public class GeneracMobileLinkGeneratorHandler extends BaseThingHandler { updateStatus(ThingStatus.UNKNOWN); } - protected void updateGeneratorStatus(GeneratorStatusDTO status) { - this.status = status; + protected void updateGeneratorStatus(Apparatus apparatus, ApparatusDetail apparatusDetail) { + this.apparatus = apparatus; + this.apparatusDetail = apparatusDetail; updateStatus(ThingStatus.ONLINE); updateState(); } - protected void updateState() { - final GeneratorStatusDTO localStatus = status; - if (localStatus != null) { - updateState("connected", OnOffType.from(localStatus.connected)); - updateState("greenLight", OnOffType.from(localStatus.greenLightLit)); - updateState("yellowLight", OnOffType.from(localStatus.yellowLightLit)); - updateState("redLight", OnOffType.from(localStatus.redLightLit)); - updateState("blueLight", OnOffType.from(localStatus.blueLightLit)); - try { - // API returns a format like 12/20/2020 - updateState("statusDate", - new DateTimeType(LocalDate - .parse(localStatus.generatorStatusDate, DateTimeFormatter.ofPattern("MM/dd/yyyy")) - .atStartOfDay(ZoneId.systemDefault()))); - } catch (IllegalArgumentException | DateTimeParseException e) { - logger.debug("Could not parse statusDate", e); - } - updateState("status", new StringType(localStatus.generatorStatus)); - updateState("currentAlarmDescription", new StringType(localStatus.currentAlarmDescription)); - updateState("runHours", new QuantityType