]> git.basschouten.com Git - openhab-addons.git/commitdiff
[generacmobilelink] Major rewrite of the Generac MobileLink Binding (#14638)
authorDan Cunningham <dan@digitaldan.com>
Sun, 9 Apr 2023 09:48:12 +0000 (02:48 -0700)
committerGitHub <noreply@github.com>
Sun, 9 Apr 2023 09:48:12 +0000 (11:48 +0200)
* [generacmobilelink] Major rewrite of the Generac MobileLink Binding

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
23 files changed:
bundles/org.openhab.binding.generacmobilelink/README.md
bundles/org.openhab.binding.generacmobilelink/pom.xml
bundles/org.openhab.binding.generacmobilelink/src/main/feature/feature.xml
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/GeneracMobileLinkBindingConstants.java
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkGeneratorConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/discovery/GeneracMobileLinkDiscoveryService.java
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Account.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/Apparatus.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusDetail.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ApparatusInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java [deleted file]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusDTO.java [deleted file]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java [deleted file]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginRequestDTO.java [deleted file]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/LoginResponseDTO.java [deleted file]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SelfAssertedResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SignInConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/factory/GeneracMobileLinkHandlerFactory.java
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkAccountHandler.java
bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/handler/GeneracMobileLinkGeneratorHandler.java
bundles/org.openhab.binding.generacmobilelink/src/main/resources/OH-INF/addon/addon.xml
bundles/org.openhab.binding.generacmobilelink/src/main/resources/OH-INF/i18n/generacmobilelink.properties
bundles/org.openhab.binding.generacmobilelink/src/main/resources/OH-INF/thing/thing-types.xml

index 4b42a9517bf9e7b76efe1819e59eddc90cab99f5..a450b3987d1f686ee7870ef370d2eb738e4bc24e 100644 (file)
@@ -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
+    }
 }
 ```
index 36c7c4722fdfb114be38b131bcb5f290065b9132..30d73c0ccd0e991d35d96479a1d78d7687d83a0d 100644 (file)
 
   <name>openHAB Add-ons :: Bundles :: GeneracMobileLink Binding</name>
 
+  <dependencies>
+    <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+      <version>1.14.3</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
 </project>
index f569b04772dcb31647e663c8070a673e9ede6c9f..f06136a2a819650c813386c807c99a889c29c938 100644 (file)
@@ -4,6 +4,7 @@
 
        <feature name="openhab-binding-generacmobilelink" description="Generac MobileLink Binding" version="${project.version}">
                <feature>openhab-runtime-base</feature>
+               <bundle dependency="true">mvn:org.jsoup/jsoup/1.14.3</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.generacmobilelink/${project.version}</bundle>
        </feature>
 </features>
index 86542ea1db5ed0dbf3dd8fe94827b797a7a85223..bf73de2815049a4862e544464c3544ac16fe9838 100644 (file)
@@ -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/config/GeneracMobileLinkGeneratorConfiguration.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/config/GeneracMobileLinkGeneratorConfiguration.java
new file mode 100644 (file)
index 0000000..a11a5b2
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * 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.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GeneracMobileLinkGeneratorConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+@NonNullByDefault
+public class GeneracMobileLinkGeneratorConfiguration {
+
+    public String generatorId = "";
+}
index f6b8fb7cf532d7eeceb994f2164ba07936680d8a..78c9c55677ce22c6699cec711b08c557fabcd0f4 100644 (file)
  */
 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 (file)
index 0000000..a3b520e
--- /dev/null
@@ -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 (file)
index 0000000..109b6b3
--- /dev/null
@@ -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<Property> 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 (file)
index 0000000..2d0f5cd
--- /dev/null
@@ -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 (file)
index 0000000..9d48a64
--- /dev/null
@@ -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/ErrorResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/ErrorResponseDTO.java
deleted file mode 100644 (file)
index c45de60..0000000
+++ /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 ErrorResponseDTO} object from the MobileLink API
- *
- * @author Dan Cunningham - Initial contribution
- */
-public class ErrorResponseDTO {
-    public Integer errorCode;
-    public String errorMessage;
-}
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 (file)
index 976be87..0000000
+++ /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/GeneratorStatusResponseDTO.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/GeneratorStatusResponseDTO.java
deleted file mode 100644 (file)
index da72108..0000000
+++ /dev/null
@@ -1,25 +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;
-
-import java.util.ArrayList;
-
-/**
- * {@link GeneratorStatusResponseDTO} response from the MobileLink API
- *
- * @author Dan Cunningham - Initial contribution
- */
-@SuppressWarnings("serial")
-public class GeneratorStatusResponseDTO extends ArrayList<GeneratorStatusDTO> {
-
-}
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 (file)
index fbba4af..0000000
+++ /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 (file)
index 401e76c..0000000
+++ /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/SelfAssertedResponse.java b/bundles/org.openhab.binding.generacmobilelink/src/main/java/org/openhab/binding/generacmobilelink/internal/dto/SelfAssertedResponse.java
new file mode 100644 (file)
index 0000000..6a895ad
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * 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 SelfAssertedResponse} represents the SelfAssertedResponse object used in login
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+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 (file)
index 0000000..87a6500
--- /dev/null
@@ -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<String, String> config;
+    public Map<String, String> 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;
+    }
+}
index e14371827da2783ee7eb029b7392f02d4a8aaa39..2faf4e1f4c4e8cb49bdc4533603c41f371b29b9c 100644 (file)
@@ -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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT,
             THING_TYPE_GENERATOR);
     private final Map<ThingUID, ServiceRegistration<?>> 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;
index 864c103d6c8d58008eff130ffe9cc0fea969d27a..a18c0a2183f853df2005e6043e9fc3144fff782d 100644 (file)
  */
 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 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<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
+                return ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString());
+            }).create();
     private HttpClient httpClient;
+    private GeneracMobileLinkDiscoveryService discoveryService;
+    private Map<String, Apparatus> apparatusesCache = new HashMap<String, Apparatus>();
     private int refreshIntervalSeconds = 60;
+    private boolean loggedIn;
+
+    private @Nullable Future<?> pollFuture;
 
-    public GeneracMobileLinkAccountHandler(Bridge bridge, HttpClient httpClient,
+    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<GeneratorStatusDTO> 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;
         }
-    }
-
-    private void stopPoll() {
-        Future<?> localPollFuture = pollFuture;
-        if (localPollFuture != null) {
-            localPollFuture.cancel(true);
+        try {
+            updateGeneratorThing(childHandler, apparatus);
+        } catch (IOException | SessionExpiredException e) {
+            logger.debug("Could not initialize child", e);
         }
     }
 
-    private void restartPoll() {
-        stopPoll();
-        pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshIntervalSeconds, TimeUnit.SECONDS);
+    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 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);
+    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;
+        }
+        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 {
-            handleErrorResponse(result);
-            if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
-                // bad credentials, stop trying to login
-                stopPoll();
+
+            String id = String.valueOf(apparatus.apparatusId);
+            apparatusesCache.put(id, apparatus);
+
+            Optional<Thing> thing = getThing().getThings().stream().filter(
+                    t -> t.getConfiguration().as(GeneracMobileLinkGeneratorConfiguration.class).generatorId.equals(id))
+                    .findFirst();
+            if (!thing.isPresent()) {
+                discoveryService.generatorDiscovered(apparatus, getThing().getUID());
+            } else {
+                ThingHandler handler = thing.get().getHandler();
+                if (handler != null) {
+                    updateGeneratorThing(handler, apparatus);
+                }
             }
         }
     }
 
-    private void getStatuses(boolean retry) throws InterruptedException {
-        if (authToken == null) {
-            return;
+    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");
         }
-        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);
+    }
+
+    private @Nullable <T> T getEndpoint(Class<T> clazz, String endpoint) throws IOException, SessionExpiredException {
+        try {
+            ContentResponse response = httpClient.newRequest(API_BASE + endpoint).send();
+            if (response.getStatus() == 204) {
+                // no data
+                return null;
             }
-        } else {
-            if (retry) {
-                logger.debug("Retrying status request");
-                getStatuses(false);
-            } else {
-                handleErrorResponse(result);
+            if (response.getStatus() != 200) {
+                throw new SessionExpiredException("API returned status code: " + response.getStatus());
             }
+            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 HTTPResult sendRequest(String url, HttpMethod method, @Nullable String token,
-            @Nullable ContentProvider content, @Nullable String contentType) throws InterruptedException {
+    /**
+     * 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 {
-            Request request = httpClient.newRequest(url).method(method).timeout(10, TimeUnit.SECONDS);
-            if (token != null) {
-                request = request.header("AuthToken", token);
+            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;
             }
-            if (content != null & contentType != null) {
-                request = request.content(content, contentType);
+
+            // 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");
             }
-            logger.trace("Sending {} to {}", request.getMethod(), request.getURI());
-            final CompletableFuture<HTTPResult> 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 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 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 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 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);
-                    }
-                }
-            });
+    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);
         }
     }
 }
index ea2bb3858b06278ba50e5c71f497c3eece25f1aa..a02fbf91a5c8bc6b2642610d28952dc7510d5356 100644 (file)
  */
 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));
+    private void updateState() {
+        Apparatus apparatus = this.apparatus;
+        ApparatusDetail apparatusDetail = this.apparatusDetail;
+        if (apparatus == null || apparatusDetail == null) {
+            return;
+        }
+        updateState(CHANNEL_HERO_IMAGE_URL, new StringType(apparatusDetail.heroImageUrl));
+        updateState(CHANNEL_STATUS_LABEL, new StringType(apparatusDetail.statusLabel));
+        updateState(CHANNEL_STATUS_TEXT, new StringType(apparatusDetail.statusText));
+        updateState(CHANNEL_ACTIVATION_DATE, new DateTimeType(apparatusDetail.activationDate));
+        updateState(CHANNEL_DEVICE_SSID, new StringType(apparatusDetail.deviceSsid));
+        updateState(CHANNEL_STATUS, new DecimalType(apparatusDetail.apparatusStatus));
+        updateState(CHANNEL_IS_CONNECTED, OnOffType.from(apparatusDetail.isConnected));
+        updateState(CHANNEL_IS_CONNECTING, OnOffType.from(apparatusDetail.isConnecting));
+        updateState(CHANNEL_SHOW_WARNING, OnOffType.from(apparatusDetail.showWarning));
+        updateState(CHANNEL_HAS_MAINTENANCE_ALERT, OnOffType.from(apparatusDetail.hasMaintenanceAlert));
+        updateState(CHANNEL_LAST_SEEN, new DateTimeType(apparatusDetail.lastSeen));
+        updateState(CHANNEL_CONNECTION_TIME, new DateTimeType(apparatusDetail.connectionTimestamp));
+        Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 70).findFirst().ifPresent(p -> {
             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(CHANNEL_RUN_HOURS, new QuantityType<Time>(Integer.parseInt(p.value), Units.HOUR));
+            } catch (NumberFormatException e) {
+                logger.debug("Could not parse runHours {}", p.value);
+                updateState(CHANNEL_RUN_HOURS, UnDefType.UNDEF);
             }
-            updateState("status", new StringType(localStatus.generatorStatus));
-            updateState("currentAlarmDescription", new StringType(localStatus.currentAlarmDescription));
-            updateState("runHours", new QuantityType<Time>(localStatus.runHours, Units.HOUR));
-            updateState("exerciseHours", new QuantityType<Time>(localStatus.exerciseHours, Units.HOUR));
-            updateState("fuelType", new DecimalType(localStatus.fuelType));
-            updateState("fuelLevel", QuantityType.valueOf(localStatus.fuelLevel, Units.PERCENT));
-            updateState("batteryVoltage", new StringType(localStatus.batteryVoltage));
-            updateState("serviceStatus", OnOffType.from(localStatus.generatorServiceStatus));
-        }
+        });
+        Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 69).findFirst().ifPresent(p -> {
+            try {
+                updateState(CHANNEL_BATTERY_VOLTAGE,
+                        new QuantityType<ElectricPotential>(Float.parseFloat(p.value), Units.VOLT));
+            } catch (NumberFormatException e) {
+                logger.debug("Could not parse batteryVoltage {}", p.value);
+                updateState(CHANNEL_BATTERY_VOLTAGE, UnDefType.UNDEF);
+            }
+        });
+        Arrays.stream(apparatusDetail.properties).filter(p -> p.type == 31).findFirst().ifPresent(p -> {
+            try {
+                updateState(CHANNEL_HOURS_OF_PROTECTION, new QuantityType<Time>(Float.parseFloat(p.value), Units.HOUR));
+            } catch (NumberFormatException e) {
+                logger.debug("Could not parse hoursOfProtection {}", p.value);
+                updateState(CHANNEL_HOURS_OF_PROTECTION, UnDefType.UNDEF);
+            }
+        });
+        apparatus.properties.stream().filter(p -> p.type == 3).findFirst().ifPresent(p -> {
+            try {
+                if (p.value.signalStrength != null) {
+                    updateState(CHANNEL_SIGNAL_STRENGH, new QuantityType<Dimensionless>(
+                            Integer.parseInt(p.value.signalStrength.replaceAll("%", "")), Units.PERCENT));
+                }
+            } catch (NumberFormatException e) {
+                logger.debug("Could not parse signalStrength {}", p.value.signalStrength);
+                updateState(CHANNEL_SIGNAL_STRENGH, UnDefType.UNDEF);
+            }
+        });
     }
 }
index c04c537b8a2245560b78e921abf7b855c824d801..bf4a515bd0ae0805c6bf71fe4562f7dda1669f92 100644 (file)
@@ -6,5 +6,6 @@
        <type>binding</type>
        <name>GeneracMobileLink Binding</name>
        <description>This binding monitors Generac manufactured generators through the MobileLink cloud service.</description>
+       <connection>cloud</connection>
 
 </addon:addon>
index 8544f3557d6206a09513bb7960bf73fe607bb0be..656aa1c3c03f8bc304485dcaa66f152a95def016 100644 (file)
@@ -23,17 +23,48 @@ thing-type.config.generacmobilelink.generator.generatorId.description = Generato
 
 # channel types
 
-channel-type.generacmobilelink.batteryVoltage.label = Battery Voltage Status
-channel-type.generacmobilelink.blueLight.label = Blue Light Status
-channel-type.generacmobilelink.connected.label = Connected
-channel-type.generacmobilelink.currentAlarmDescription.label = Current Alarm Description
-channel-type.generacmobilelink.exerciseHours.label = Number of Hours Exercised
-channel-type.generacmobilelink.fuelLevel.label = Fuel Level
-channel-type.generacmobilelink.fuelType.label = Fuel Type
-channel-type.generacmobilelink.greenLight.label = Green Light Status
-channel-type.generacmobilelink.redLight.label = Red Light Status
-channel-type.generacmobilelink.runHours.label = Number of Hours Run
-channel-type.generacmobilelink.serviceStatus.label = Service Status
+channel-type.generacmobilelink.activationDate.label = Activation Date
+channel-type.generacmobilelink.activationDate.description = The activation date of the generator.
+channel-type.generacmobilelink.batteryVoltage.label = Battery Voltage
+channel-type.generacmobilelink.batteryVoltage.description = The battery voltage.
+channel-type.generacmobilelink.connectionTime.label = Connection Time
+channel-type.generacmobilelink.connectionTime.description = The date that the unit has been connected from.
+channel-type.generacmobilelink.deviceSsid.label = Device SSID
+channel-type.generacmobilelink.deviceSsid.description = The SSID that the generator broadcasts for setup.
+channel-type.generacmobilelink.hasMaintenanceAlert.label = Has Maintenance Alert
+channel-type.generacmobilelink.hasMaintenanceAlert.description = Does the generator require maintenance.
+channel-type.generacmobilelink.heroImageUrl.label = Hero Image URL
+channel-type.generacmobilelink.heroImageUrl.description = URL to an image of the generator.
+channel-type.generacmobilelink.hoursOfProtection.label = Hours of Protection
+channel-type.generacmobilelink.hoursOfProtection.description = Number of hours of protection the generator has provided.
+channel-type.generacmobilelink.isConnected.label = Is Connected
+channel-type.generacmobilelink.isConnected.description = Is the unit connected to the cloud service.
+channel-type.generacmobilelink.isConnecting.label = Is Connecting
+channel-type.generacmobilelink.isConnecting.description = Is the unit connecting to the cloud service.
+channel-type.generacmobilelink.lastSeen.label = Last Seen
+channel-type.generacmobilelink.lastSeen.description = The date that the unit was last connected to the cloud service.
+channel-type.generacmobilelink.runHours.label = Run Hours
+channel-type.generacmobilelink.runHours.description = Number of hours run.
+channel-type.generacmobilelink.showWarning.label = Show Warning
+channel-type.generacmobilelink.showWarning.description = Should a user interface show a warning symbol due to the current status.
+channel-type.generacmobilelink.signalStrength.label = Signal Strength
+channel-type.generacmobilelink.signalStrength.description = The Wi-Fi signal strength of the generator
 channel-type.generacmobilelink.status.label = Status
-channel-type.generacmobilelink.statusDate.label = Last Status Date
-channel-type.generacmobilelink.yellowLight.label = Yellow Light Status
+channel-type.generacmobilelink.status.description = The current status of the generator.
+channel-type.generacmobilelink.status.state.option.1 = Ready
+channel-type.generacmobilelink.status.state.option.2 = Running
+channel-type.generacmobilelink.status.state.option.3 = Exercising
+channel-type.generacmobilelink.status.state.option.4 = Warning
+channel-type.generacmobilelink.status.state.option.5 = Stopped
+channel-type.generacmobilelink.status.state.option.6 = Communication Issue
+channel-type.generacmobilelink.status.state.option.7 = Unknown
+channel-type.generacmobilelink.statusLabel.label = Status Label
+channel-type.generacmobilelink.statusLabel.description = The label used to identify the current status.
+channel-type.generacmobilelink.statusText.label = Status Text
+channel-type.generacmobilelink.statusText.description = The longer description of the current status.
+
+# things
+
+thing.generacmobilelink.account.offline.communication-error.session-expired = Session Expired
+thing.generacmobilelink.account.offline.configuration-error.invalid-credentials = Invalid Credentials
+thing.generacmobilelink.account.offline.communication-error.io-exception = Error Communicating with Service
index 020894830779f89e02a12b023efc7007ae64bd62..418369d0da96d822e7935f4757e63416ff1c782a 100644 (file)
                <label>MobileLink Generator</label>
                <description>MobileLink Generator</description>
                <channels>
-                       <channel id="connected" typeId="connected"/>
-                       <channel id="greenLight" typeId="greenLight"/>
-                       <channel id="yellowLight" typeId="yellowLight"/>
-                       <channel id="redLight" typeId="redLight"/>
-                       <channel id="blueLight" typeId="blueLight"/>
-                       <channel id="statusDate" typeId="statusDate"/>
+                       <channel id="heroImageUrl" typeId="heroImageUrl"/>
+                       <channel id="statusLabel" typeId="statusLabel"/>
+                       <channel id="statusText" typeId="statusText"/>
+                       <channel id="activationDate" typeId="activationDate"/>
+                       <channel id="deviceSsid" typeId="deviceSsid"/>
                        <channel id="status" typeId="status"/>
-                       <channel id="currentAlarmDescription" typeId="currentAlarmDescription"/>
+                       <channel id="isConnected" typeId="isConnected"/>
+                       <channel id="isConnecting" typeId="isConnecting"/>
+                       <channel id="showWarning" typeId="showWarning"/>
+                       <channel id="hasMaintenanceAlert" typeId="hasMaintenanceAlert"/>
+                       <channel id="lastSeen" typeId="lastSeen"/>
+                       <channel id="connectionTime" typeId="connectionTime"/>
                        <channel id="runHours" typeId="runHours"/>
-                       <channel id="exerciseHours" typeId="exerciseHours"/>
-                       <channel id="fuelType" typeId="fuelType"/>
-                       <channel id="fuelLevel" typeId="fuelLevel"/>
                        <channel id="batteryVoltage" typeId="batteryVoltage"/>
-                       <channel id="serviceStatus" typeId="serviceStatus"/>
+                       <channel id="hoursOfProtection" typeId="hoursOfProtection"/>
+                       <channel id="signalStrength" typeId="signalStrength"/>
                </channels>
                <representation-property>generatorId</representation-property>
                <config-description-ref uri="thing-type:generacmobilelink:generator"/>
        </thing-type>
 
-       <channel-type id="connected">
-               <item-type>Switch</item-type>
-               <label>Connected</label>
+       <channel-type id="status">
+               <item-type>Number</item-type>
+               <label>Status</label>
+               <description>The current status of the generator.</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="1">Ready</option>
+                               <option value="2">Running</option>
+                               <option value="3">Exercising</option>
+                               <option value="4">Warning</option>
+                               <option value="5">Stopped</option>
+                               <option value="6">Communication Issue</option>
+                               <option value="7">Unknown</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="statusLabel">
+               <item-type>String</item-type>
+               <label>Status Label</label>
+               <description>The label used to identify the current status.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="greenLight">
-               <item-type>Switch</item-type>
-               <label>Green Light Status</label>
+       <channel-type id="statusText">
+               <item-type>String</item-type>
+               <label>Status Text</label>
+               <description>The longer description of the current status.</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="heroImageUrl">
+               <item-type>String</item-type>
+               <label>Hero Image URL</label>
+               <description>URL to an image of the generator.</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="activationDate">
+               <item-type>DateTime</item-type>
+               <label>Activation Date</label>
+               <description>The activation date of the generator.</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="deviceSsid">
+               <item-type>String</item-type>
+               <label>Device SSID</label>
+               <description>The SSID that the generator broadcasts for setup.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="yellowLight">
+       <channel-type id="isConnected">
                <item-type>Switch</item-type>
-               <label>Yellow Light Status</label>
+               <label>Is Connected</label>
+               <description>Is the unit connected to the cloud service.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="redLight">
+       <channel-type id="isConnecting">
                <item-type>Switch</item-type>
-               <label>Red Light Status</label>
+               <label>Is Connecting</label>
+               <description>Is the unit connecting to the cloud service.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="blueLight">
+       <channel-type id="showWarning">
                <item-type>Switch</item-type>
-               <label>Blue Light Status</label>
+               <label>Show Warning</label>
+               <description>Should a user interface show a warning symbol due to the current status.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="statusDate">
-               <item-type>DateTime</item-type>
-               <label>Last Status Date</label>
+       <channel-type id="hasMaintenanceAlert">
+               <item-type>Switch</item-type>
+               <label>Has Maintenance Alert</label>
+               <description>Does the generator require maintenance.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="status">
-               <item-type>String</item-type>
-               <label>Status</label>
+       <channel-type id="lastSeen">
+               <item-type>DateTime</item-type>
+               <label>Last Seen</label>
+               <description>The date that the unit was last connected to the cloud service.</description>
                <state readOnly="true"/>
        </channel-type>
-       <channel-type id="currentAlarmDescription">
-               <item-type>String</item-type>
-               <label>Current Alarm Description</label>
+       <channel-type id="connectionTime">
+               <item-type>DateTime</item-type>
+               <label>Connection Time</label>
+               <description>The date that the unit has been connected from.</description>
                <state readOnly="true"/>
        </channel-type>
        <channel-type id="runHours">
                <item-type>Number:Time</item-type>
-               <label>Number of Hours Run</label>
+               <label>Run Hours</label>
+               <description>Number of hours run.</description>
                <state readOnly="true" pattern="%d %unit%"/>
        </channel-type>
-       <channel-type id="exerciseHours">
-               <item-type>Number:Time</item-type>
-               <label>Number of Hours Exercised</label>
+       <channel-type id="batteryVoltage">
+               <item-type>Number:ElectricPotential</item-type>
+               <label>Battery Voltage</label>
+               <description>The battery voltage.</description>
                <state readOnly="true" pattern="%d %unit%"/>
        </channel-type>
-       <channel-type id="fuelType">
-               <item-type>Number</item-type>
-               <label>Fuel Type</label>
-               <state readOnly="true"/>
+       <channel-type id="hoursOfProtection">
+               <item-type>Number:Time</item-type>
+               <label>Hours of Protection</label>
+               <description>Number of hours of protection the generator has provided.</description>
+               <state readOnly="true" pattern="%d %unit%"/>
        </channel-type>
-       <channel-type id="fuelLevel">
+       <channel-type id="signalStrength">
                <item-type>Number:Dimensionless</item-type>
-               <label>Fuel Level</label>
-               <state readOnly="true"/>
-       </channel-type>
-       <channel-type id="batteryVoltage">
-               <item-type>String</item-type>
-               <label>Battery Voltage Status</label>
-               <state readOnly="true"/>
-       </channel-type>
-       <channel-type id="serviceStatus">
-               <item-type>Switch</item-type>
-               <label>Service Status</label>
-               <state readOnly="true"/>
+               <label>Signal Strength</label>
+               <description>The Wi-Fi signal strength of the generator</description>
+               <state readOnly="true" pattern="%d %unit%"/>
        </channel-type>
 </thing:thing-descriptions>