[Easee] Initial contribution (#12954)

* initial binding version

Signed-off-by: Alexander Friese <af944580@gmail.com>
This commit is contained in:
alexf2015 2022-09-10 16:57:21 +02:00 committed by GitHub
parent dce6dc9f79
commit 1f1de41bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 4555 additions and 0 deletions

View File

@ -376,6 +376,11 @@
<artifactId>org.openhab.binding.dwdunwetter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.easee</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ecobee</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,178 @@
# Easee Binding
The Easee binding can be used to retrieve data from the Easee Cloud API and also to control your wallbox via the Cloud API.
This allows you to dynamically adjust the charge current for your car depending on production of your solar plant.
## Supported Things
This binding provides three thing types:
| Thing/Bridge | Thing Type | Description |
|---------------------|---------------------|-----------------------------------------------------------------------------------------------|
| bridge | site | cloud connection to a site within an Easee account |
| thing | charger | the physical charger which is connected to a circuit within the given site |
| thing | mastercharger | like the "normal" charger but with additional capability to control the circuit |
Basically any Easee wallbox that supports the Cloud API should automatically be supported by this binding.
## Discovery
Auto-discovery is supported and will discover all circuits and chargers assigned to a given site.
## Bridge Configuration
The following configuration parameters are available for the binding/bridge:
| Configuration Parameter | Required | Description |
|-------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| username | yes | The username to login at Easee Cloud service. This should be an e-mail address or phone number. |
| passord | yes | Your password to login at Easee Cloud service. |
| siteId | yes | The ID of the site containing the wallbox(es) and circuit(s) that should be integrated into openHAB. The ID of your site can be found via the sites overview (https://easee.cloud/sites). You just need to click one of the sites listed there, the id will be part of the URL which is then opened. It will be a number with typically 6 digits. |
| dataPollingInterval | no | Interval (seconds) in which live data values are retrieved from the Easee Cloud API. (default = 120) |
## Thing configuration
It is recommended to use auto discovery which does not require further configuration.
If manual configuration is preferred you need to specify configuration as below.
### Charger
| Configuration Parameter | Required | Description |
|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------|
| id | yes | The id of the charger that will be represented by this thing. |
### Mastercharger
| Configuration Parameter | Required | Description |
|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------|
| id | yes | The id of the charger that will be represented by this thing. |
| circuitId | yes | The id of the circuit that is controlled by this charger. |
## Channels
The binding only supports a subset of the available endpoints provided by the Easee Cloud API.
The tables below show all available channels and which of them are writable.
The settings that start with "dynamic" can be changed frequently, the others are written to flash and thus should not be changed too often as this could result in damage of your flash.
### Charger Channels
| Channel | Item Type | Writable | Description | Allowed Values (write access) |
|---------------------------------------------|--------------------------|----------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| state#smartCharging | Switch | no | | |
| state#cableLocked | Switch | no | | |
| state#chargerOpMode | Number | no | | |
| state#totalPower | Number:Power | no | current session total power (all phases) | |
| state#sessionEnergy | Number:Energy | no | current session | |
| state#dynamicCircuitCurrentP1 | Number:ElectricCurrent | no | | |
| state#dynamicCircuitCurrentP2 | Number:ElectricCurrent | no | | |
| state#dynamicCircuitCurrentP3 | Number:ElectricCurrent | no | | |
| state#latestPulse | DateTime | no | | |
| state#chargerFirmware | Number | no | | |
| state#latestFirmware | Number | no | | |
| state#voltage | Number:ElectricPotential | no | | |
| state#outputCurrent | Number:ElectricCurrent | no | | |
| state#isOnline | Switch | no | | |
| state#dynamicChargerCurrent | Number:ElectricCurrent | yes | | 0, 6-32 |
| state#reasonForNoCurrent | Number | no | | |
| state#lifetimeEnergy | Number:Energy | no | | |
| state#errorCode | Number | no | | |
| state#fatalErrorCode | Number | no | | |
| config#lockCablePermanently | Switch | yes | | true/false |
| config#authorizationRequired | Switch | yes | | true/false |
| config#limitToSinglePhaseCharging | Switch | yes | | true/false |
| config#phaseMode | Number | yes | 1=1phase, 2=auto, 3=3phase | 1-3 |
| config#maxChargerCurrent | Number:ElectricCurrent | no | write access not yet implemented | |
| commands#genericCommand | String | yes | Generic Endpoint to send commands | reboot, update_firmware, poll_all, smart_charging, start_charging, stop_charging, pause_charging, resume_charging, toggle_charging, override_schedule |
| commands#startStop | Switch | yes | Start/Stop Charing, only works with authorization | |
| commands#pauseResume | Switch | yes | Pause/Resume Charing | |
| latestSession#sessionEnergy | Number:Energy | no | latest (already ended) session | |
| latestSession#sessionStart | DateTime | no | | |
| latestSession#sessionEnd | DateTime | no | | |
### Master Charger Channels
The Master Charger is like the "normal" charger but has some extra channels to control the circuit. These additional channels are listed in the table below.
| Channel | Item Type | Writable | Description | Allowed Values (write access) |
|---------------------------------------------|--------------------------|----------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| dynamicCurrent#phase1 | Number:ElectricCurrent | no | | |
| dynamicCurrent#phase2 | Number:ElectricCurrent | no | | |
| dynamicCurrent#phase3 | Number:ElectricCurrent | no | | |
| dynamicCurrent#dynamicCurrents | String | yes | read/write only for all phases. | <value phase1>;<value phase2>;<value phase3> valid values for each phase are 0, 6-32 |
| settings#maxCircuitCurrentP1 | Number:ElectricCurrent | no | | |
| settings#maxCircuitCurrentP2 | Number:ElectricCurrent | no | | |
| settings#maxCircuitCurrentP3 | Number:ElectricCurrent | no | | |
| settings#maxCurrents | String | yes | read/write only for all phases. | <value phase1>;<value phase2>;<value phase3> valid values for each phase are 0, 6-32 |
| settings#offlineMaxCircuitCurrentP1 | Number:ElectricCurrent | no | | |
| settings#offlineMaxCircuitCurrentP2 | Number:ElectricCurrent | no | | |
| settings#offlineMaxCircuitCurrentP3 | Number:ElectricCurrent | no | | |
| settings#offlineMaxCurrents | String | yes | read/write only for all phases. | <value phase1>;<value phase2>;<value phase3> valid values for each phase are 0, 6-32 |
| settings#enableIdleCurrent | Switch | yes | | true/false |
| settings#allowOfflineMaxCircuitCurrent | Switch | no | | |
## Full Example
### Thing
#### Minimum configuration
```
Bridge easee:site:mysite1 [ username="abc@def.net", password="secret", siteId="123456" ]
```
#### Manual configuration with two chargers, pollingInterval set to 60 seconds.
```
Bridge easee:site:mysite1 [ username="abc@def.net", password="secret", siteId="471111", dataPollingInterval=60 ] {
Thing mastercharger myCharger1 [ id="EHXXXXX1", circuitId="1234567" ]
Thing charger myCharger2 [ id="EHXXXXX2" ]
}
```
### Items
```
Number:ElectricCurrent Easee_Circuit_Phase1 "Phase 1" { channel="easee:mastercharger:mysite1:myCharger1:dynamicCurrent#phase1" }
Number:ElectricCurrent Easee_Circuit_Phase2 "Phase 2" { channel="easee:mastercharger:mysite1:myCharger1:dynamicCurrent#phase2" }
Number:ElectricCurrent Easee_Circuit_Phase3 "Phase 3" { channel="easee:mastercharger:mysite1:myCharger1:dynamicCurrent#phase3" }
String Easee_Circuit_Dynamic_Phases "Dynamic Power [MAP(easeePhases.map):%s]" { channel="easee:mastercharger:mysite1:myCharger1:dynamicCurrent#setDynamicCurrents" }
Switch Easee_Charger_Start_Stop "Start / Stop" { channel="easee:mastercharger:mysite1:myCharger1:commands#startStop" }
```
### Sitemap
```
Switch item=Easee_Circuit_Dynamic_Phases mappings=["0;0;0"="0.00 kW", "6;0;0"="1.44 kW", "7;0;0"="1.68 kW", "8;0;0"="1.92 kW", "9;0;0"="2.16 kW", "10;0;0"="2.40 kW", "16;0;0"="3.72 kW", "16;16;16"="11.1 kW"] icon="energy"
```
### Mapping
easeePhases.map will make the phase setting more readable.
```
0;0;0=0.00 kW
6;0;0=1.44 kW
7;0;0=1.68 kW
8;0;0=1.92 kW
9;0;0=2.16 kW
10;0;0=2.40 kW
11;0;0=2.64 kW
12;0;0=2.88 kW
13;0;0=3.12 kW
14;0;0=3.36 kW
15;0;0=3.60 kW
16;0;0=3.72 kW
6;6;6=4.32 kW
7;7;7=5.04 kW
8;8;8=5.76 kW
9;9;9=6.48 kW
10;10;10=7.20 kW
11;11;11=7.92 kW
12;12;12=8.64 kW
13;13;13=9.36 kW
14;14;14=10.1 kW
15;15;15=10.8 kW
16;16;16=11.1 kW
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.4.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.easee</artifactId>
<name>openHAB Add-ons :: Bundles :: Easee Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.easee-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-easee" description="Easee Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.easee/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* trait class which contains useful helper methods. Thus, the interface can be implemented and methods are available
* within the class.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public interface AtomicReferenceTrait {
/**
* this should usually not called directly. use updateJobReference or cancelJobReference instead
*
* @param job job to cancel.
*/
default void cancelJob(@Nullable Future<?> job) {
if (job != null) {
job.cancel(true);
}
}
/**
* updates a job reference with a new job. the old job will be cancelled if there is one.
*
* @param jobReference reference to be updated
* @param newJob job to be assigned
*/
default void updateJobReference(AtomicReference<@Nullable Future<?>> jobReference, Future<?> newJob) {
cancelJob(jobReference.getAndSet(newJob));
}
/**
* updates a job reference to null and cancels any existing job which might be assigned to the reference.
*
* @param jobReference to be updated to null.
*/
default void cancelJobReference(AtomicReference<@Nullable Future<?>> jobReference) {
cancelJob(jobReference.getAndSet(null));
}
}

View File

@ -0,0 +1,172 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal;
import java.time.Instant;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link EaseeBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Alexander Friese - Initial contribution
*/
@NonNullByDefault
public class EaseeBindingConstants {
public static final String BINDING_ID = "easee";
// List of main device types
public static final String DEVICE_SITE = "site";
public static final String DEVICE_MASTER_CHARGER = "mastercharger";
public static final String DEVICE_CHARGER = "charger";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SITE = new ThingTypeUID(BINDING_ID, DEVICE_SITE);
public static final ThingTypeUID THING_TYPE_MASTER_CHARGER = new ThingTypeUID(BINDING_ID, DEVICE_MASTER_CHARGER);
public static final ThingTypeUID THING_TYPE_CHARGER = new ThingTypeUID(BINDING_ID, DEVICE_CHARGER);
// List of all channel groups
public static final String CHANNEL_GROUP_NONE = "";
public static final String CHANNEL_GROUP_SITE_INFO = "info";
public static final String CHANNEL_GROUP_CHARGER = "charger";
public static final String CHANNEL_GROUP_CHARGER_STATE = "state";
public static final String CHANNEL_GROUP_CHARGER_CONFIG = "config";
public static final String CHANNEL_GROUP_CHARGER_COMMANDS = "commands";
public static final String CHANNEL_GROUP_CHARGER_LATEST_SESSION = "latestSession";
public static final String CHANNEL_GROUP_CIRCUIT_DYNAMIC_CURRENT = "dynamicCurrent";
public static final String CHANNEL_GROUP_CIRCUIT_SETTINGS = "settings";
// Channel types
public static final String CHANNEL_TYPE_SWITCH = "Switch";
public static final String CHANNEL_TYPE_VOLT = "Number:ElectricPotential";
public static final String CHANNEL_TYPE_AMPERE = "Number:ElectricCurrent";
public static final String CHANNEL_TYPE_KWH = "Number:Energy";
public static final String CHANNEL_TYPE_KW = "Number:Power";
public static final String CHANNEL_TYPE_DATE = "DateTime";
public static final String CHANNEL_TYPE_STRING = "String";
public static final String CHANNEL_TYPE_NUMBER = "Number";
public static final String CHANNEL_TYPEPREFIX_RW = "rw";
public static final String CHANNEL_TYPENAME_INTEGER = "type-integer";
// Channels with specific handling
public static final String CHANNEL_CHARGER_OP_MODE = "chargerOpMode";
public static final String CHANNEL_CHARGER_DYNAMIC_CURRENT = "dynamicChargerCurrent";
public static final String CHANNEL_CHARGER_REASON_FOR_NO_CURRENT = "reasonForNoCurrent";
public static final String CHANNEL_CHARGER_START_STOP = "startStop";
public static final String CHANNEL_CHARGER_PAUSE_RESUME = "pauseResume";
public static final String CHANNEL_CIRCUIT_DYNAMIC_CURRENTS = "dynamicCurrents";
public static final String CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE1 = "phase1";
public static final String CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE2 = "phase2";
public static final String CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE3 = "phase3";
public static final String CHANNEL_CIRCUIT_MAX_CURRENTS = "maxCurrents";
public static final String CHANNEL_CIRCUIT_MAX_CURRENT_PHASE1 = "maxCircuitCurrentP1";
public static final String CHANNEL_CIRCUIT_MAX_CURRENT_PHASE2 = "maxCircuitCurrentP2";
public static final String CHANNEL_CIRCUIT_MAX_CURRENT_PHASE3 = "maxCircuitCurrentP3";
public static final String CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENTS = "offlineMaxCurrents";
public static final String CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE1 = "offlineMaxCircuitCurrentP1";
public static final String CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE2 = "offlineMaxCircuitCurrentP2";
public static final String CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE3 = "offlineMaxCircuitCurrentP3";
// JSON Keys
public static final String JSON_KEY_GENERIC_ID = "id";
public static final String JSON_KEY_GENERIC_NAME = "name";
public static final String JSON_KEY_CIRCUIT_NAME = "panelName";
public static final String JSON_KEY_CIRCUIT_ID = "circuitId";
public static final String JSON_KEY_CIRCUITS = "circuits";
public static final String JSON_KEY_CHARGERS = "chargers";
public static final String JSON_KEY_BACK_PLATE = "backPlate";
public static final String JSON_KEY_MASTER_BACK_PLATE = "masterBackPlate";
public static final String JSON_KEY_MASTER_BACK_PLATE_ID = "masterBackPlateId";
public static final String JSON_KEY_ONLINE = "isOnline";
public static final String JSON_KEY_SITE_KEY = "siteKey";
public static final String JSON_KEY_ERROR_TITLE = "title";
public static final String JSON_KEY_AUTH_ACCESS_TOKEN = "accessToken";
public static final String JSON_KEY_AUTH_REFRESH_TOKEN = "refreshToken";
public static final String JSON_KEY_AUTH_EXPIRES_IN = "expiresIn";
// Write Commands
public static final String COMMAND_CHANGE_CONFIGURATION = "ChangeConfiguration";
public static final String COMMAND_SEND_COMMAND = "SendCommand";
public static final String COMMAND_SEND_COMMAND_START_STOP = "SendCommandStartStop";
public static final String COMMAND_SET_CIRCUIT_SETTINGS = "SetCircuitSettings";
public static final String COMMAND_SET_DYNAMIC_CIRCUIT_CURRENTS = "SetDynamicCircuitCurrents";
public static final String COMMAND_SET_MAX_CIRCUIT_CURRENTS = "SetMaxCircuitCurrents";
public static final String COMMAND_SET_OFFLINE_MAX_CIRCUIT_CURRENTS = "SetOfflineMaxCircuitCurrents";
// Command Values
public static final String CMD_VAL_START_CHARGING = "start_charging";
public static final String CMD_VAL_STOP_CHARGING = "stop_charging";
public static final String CMD_VAL_PAUSE_CHARGING = "pause_charging";
public static final String CMD_VAL_RESUME_CHARGING = "resume_charging";
// web request constants
public static final long WEB_REQUEST_INITIAL_DELAY = 30;
public static final long WEB_REQUEST_INTERVAL = 5;
public static final int WEB_REQUEST_QUEUE_MAX_SIZE = 20;
public static final int WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES = 5;
public static final int WEB_REQUEST_TOKEN_MAX_AGE_MINUTES = 60;
public static final String WEB_REQUEST_BEARER_TOKEN_PREFIX = "Bearer ";
// URLs
public static final String LOGIN_URL = "https://api.easee.cloud/api/accounts/login";
public static final String REFRESH_TOKEN_URL = "https://api.easee.cloud/api/accounts/refresh_token";
public static final String GET_SITE_URL = "https://api.easee.cloud/api/sites/{siteId}";
public static final String CHARGER_URL = "https://api.easee.cloud/api/chargers/{id}";
public static final String STATE_URL = "https://api.easee.cloud/api/chargers/{id}/state";
public static final String GET_CONFIGURATION_URL = "https://api.easee.cloud/api/chargers/{id}/config";
public static final String CHANGE_CONFIGURATION_URL = "https://api.easee.cloud/api/chargers/{id}/settings";
public static final String COMMANDS_URL = "https://api.easee.cloud/api/chargers/{id}/commands/{command}";
public static final String LATEST_CHARGING_SESSION_URL = "https://api.easee.cloud/api/chargers/{id}/sessions/latest";
public static final String DYNAMIC_CIRCUIT_CURRENT_URL = "https://api.easee.cloud/api/sites/{siteId}/circuits/{circuitId}/dynamicCurrent";
public static final String CIRCUIT_SETTINGS_URL = "https://api.easee.cloud/api/sites/{siteId}/circuits/{circuitId}/settings";
// Status Keys
public static final String STATUS_TOKEN_VALIDATED = "@text/status.token.validated";
public static final String STATUS_WAITING_FOR_BRIDGE = "@text/status.waiting.for.bridge";
public static final String STATUS_WAITING_FOR_LOGIN = "@text/status.waiting.for.login";
public static final String STATUS_NO_VALID_DATA = "@text/status.no.valid.data";
public static final String STATUS_NO_CONNECTION = "@text/status.no.connection";
// other
public static final long POLLING_INITIAL_DELAY = 1;
public static final String GENERIC_YES = "Yes";
public static final String GENERIC_NO = "No";
public static final int CHARGER_OP_STATE_WAITING = 2;
public static final int CHARGER_OP_STATE_CHARGING = 3;
public static final int CHARGER_DYNAMIC_CURRENT_PAUSE = 0;
public static final int CHARGER_REASON_FOR_NO_CURRENT_PAUSED = 52;
public static final String THING_CONFIG_ID = "id";
public static final String THING_CONFIG_SITE_ID = "siteId";
public static final String THING_CONFIG_CIRCUIT_ID = "circuitId";
public static final String THING_CONFIG_CIRCUIT_NAME = "circuitName";
public static final String THING_CONFIG_IS_MASTER = "isMaster";
public static final String THING_CONFIG_BACK_PLATE_ID = "backPlateId";
public static final String THING_CONFIG_MASTER_BACK_PLATE_ID = "masterBackPlateId";
public static final Instant OUTDATED_DATE = Instant.MIN;
public static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String PARAMETER_NAME_WRITE_COMMAND = "writeCommand";
public static final String PARAMETER_NAME_VALIDATION_REGEXP = "validationExpression";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SITE,
THING_TYPE_MASTER_CHARGER, THING_TYPE_CHARGER);
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.easee.internal.handler.EaseeChargerHandler;
import org.openhab.binding.easee.internal.handler.EaseeMasterChargerHandler;
import org.openhab.binding.easee.internal.handler.EaseeSiteHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EaseeHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Alexander Friese - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.easee", service = ThingHandlerFactory.class)
public class EaseeHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(EaseeHandlerFactory.class);
/**
* the shared http client
*/
private final HttpClient httpClient;
@Activate
public EaseeHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_SITE.equals(thingTypeUID)) {
return new EaseeSiteHandler((Bridge) thing, httpClient);
} else if (THING_TYPE_MASTER_CHARGER.equals(thingTypeUID)) {
return new EaseeMasterChargerHandler(thing);
} else if (THING_TYPE_CHARGER.equals(thingTypeUID)) {
return new EaseeChargerHandler(thing);
} else {
logger.warn("Unsupported Thing-Type: {}", thingTypeUID.getAsString());
}
return null;
}
}

View File

@ -0,0 +1,196 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.easee.internal.model.ConfigurationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
/**
* some helper methods.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public final class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
/**
* only static methods no instance needed
*/
private Utils() {
}
/**
* parses a date string in easee format to ZonedDateTime which is used by Openhab.
*
* @param date
* @return
*/
public static ZonedDateTime parseDate(String date) throws DateTimeParseException {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX");
LOGGER.trace("parsing: {}", date);
ZonedDateTime zdt = ZonedDateTime.parse(date, formatter);
return zdt;
}
/**
* returns a date in a readable format
*
* @param date
* @return
*/
public static String formatDate(Instant date) {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
return formatter.format(date);
}
/**
* get element as JsonObject.
*
* @param jsonObject
* @param key
* @return
*/
public static @Nullable JsonObject getAsJsonObject(@Nullable JsonObject jsonObject, String key) {
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
return (element instanceof JsonObject) ? element.getAsJsonObject() : null;
}
/**
* get element as String.
*
* @param jsonObject
* @param key
* @return
*/
public static @Nullable String getAsString(@Nullable JsonObject jsonObject, String key) {
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
String text = null;
if (element != null) {
if (element instanceof JsonPrimitive) {
text = element.getAsString();
} else if (element instanceof JsonObject || element instanceof JsonArray) {
text = element.toString();
}
}
return text;
}
/**
* get element as int.
*
* @param jsonObject
* @param key
* @return
*/
public static int getAsInt(@Nullable JsonObject jsonObject, String key) {
JsonElement element = jsonObject == null ? null : jsonObject.get(key);
return (element instanceof JsonPrimitive) ? element.getAsInt() : 0;
}
/**
* get element as boolean.
*
* @param jsonObject
* @param key
* @return
*/
public static @Nullable Boolean getAsBool(@Nullable JsonObject jsonObject, String key) {
JsonElement json = jsonObject == null ? null : jsonObject.get(key);
return (json == null || json instanceof JsonNull) ? null : json.getAsBoolean();
}
/**
* retrieves typeID of a channel.
*
* @param channel
* @return typeID or empty string if typeUID is null.
*/
public static String getChannelTypeId(Channel channel) {
ChannelTypeUID typeUID = channel.getChannelTypeUID();
if (typeUID == null) {
return "";
}
return typeUID.getId();
}
/**
* retrieves the validation expression which is assigned to this channel, fallback to a public static, if no
* validation
* is
* defined.
*
* @param channel
* @return the validation expression
*/
public static String getValidationExpression(Channel channel) {
String expr = getPropertyOrParameter(channel, PARAMETER_NAME_VALIDATION_REGEXP);
if (expr == null) {
throw new ConfigurationException(
"channel (" + channel.getUID().getId() + ") does not have a validation expression configured");
}
return expr;
}
/**
* retrieves the write API url suffix which is assigned to this channel.
*
* @param channel
* @return the url suffix
*/
public static String getWriteCommand(Channel channel) {
String command = getPropertyOrParameter(channel, PARAMETER_NAME_WRITE_COMMAND);
if (command == null) {
throw new ConfigurationException(
"channel (" + channel.getUID().getId() + ") does not have a write command configured");
}
return command;
}
/**
* internal utiliy method which returns a property (if found) or a config parameter (if found) otherwise null
*
* @param channel
* @param name
* @return
*/
public static @Nullable String getPropertyOrParameter(Channel channel, String name) {
String value = channel.getProperties().get(name);
// also eclipse says this cannot be null, it definitely can!
if (value == null || value.isEmpty()) {
Object obj = channel.getConfiguration().get(name);
value = obj == null ? null : obj.toString();
}
return value;
}
}

View File

@ -0,0 +1,328 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.WEB_REQUEST_BEARER_TOKEN_PREFIX;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpStatus.Code;
import org.openhab.binding.easee.internal.EaseeBindingConstants;
import org.openhab.binding.easee.internal.connector.CommunicationStatus;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.GenericResponseTransformer;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.ToNumberPolicy;
/**
* base class for all commands. common logic should be implemented here
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public abstract class AbstractCommand extends BufferingResponseListener implements EaseeCommand {
public static enum RetryOnFailure {
YES,
NO;
}
public static enum ProcessFailureResponse {
YES,
NO;
}
/**
* logger
*/
private final Logger logger = LoggerFactory.getLogger(AbstractCommand.class);
/**
* the configuration
*/
protected final EaseeThingHandler handler;
/**
* JSON deserializer
*/
protected final Gson gson;
/**
* status code of fulfilled request
*/
private final CommunicationStatus communicationStatus;
/**
* generic transformer which just transfers all values in a plain map.
*/
private final GenericResponseTransformer transformer;
/**
* retry counter.
*/
private int retries = 0;
/**
* retry active
*/
private final RetryOnFailure retryOnFailure;
/**
* process error response, e.g. set handler offline on error
*/
private final ProcessFailureResponse processFailureResponse;
/**
* allows further processing of the json result data, if set.
*/
private List<JsonResultProcessor> resultProcessors;
/**
* the constructor
*/
public AbstractCommand(EaseeThingHandler handler, RetryOnFailure retryOnFailure,
ProcessFailureResponse processFailureResponse) {
this.gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
this.communicationStatus = new CommunicationStatus();
this.resultProcessors = new ArrayList<>();
this.transformer = new GenericResponseTransformer(handler);
this.handler = handler;
this.processFailureResponse = processFailureResponse;
this.retryOnFailure = retryOnFailure;
}
/**
* the constructor
*/
public AbstractCommand(EaseeThingHandler handler, RetryOnFailure retryOnFailure,
ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
this(handler, retryOnFailure, processFailureResponse);
this.resultProcessors.add(resultProcessor);
}
/**
* Log request success
*/
@Override
public final void onSuccess(@Nullable Response response) {
super.onSuccess(response);
if (response != null) {
communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
logger.debug("HTTP response {}", response.getStatus());
}
}
/**
* Log request failure
*/
@Override
public final void onFailure(@Nullable Response response, @Nullable Throwable failure) {
super.onFailure(response, failure);
if (failure != null) {
logger.info("Request failed: {}", failure.toString());
communicationStatus.setError((Exception) failure);
if (failure instanceof SocketTimeoutException || failure instanceof TimeoutException) {
communicationStatus.setHttpCode(Code.REQUEST_TIMEOUT);
} else if (failure instanceof UnknownHostException) {
communicationStatus.setHttpCode(Code.BAD_GATEWAY);
} else {
communicationStatus.setHttpCode(Code.INTERNAL_SERVER_ERROR);
}
} else {
logger.info("Request failed");
}
if (response != null && response.getStatus() > 0) {
communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
}
}
/**
* just for logging of content
*/
@Override
public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
super.onContent(response, content);
logger.debug("received content, length: {}", getContentAsString().length());
}
/**
* default handling of successful requests.
*/
@Override
public void onComplete(@Nullable Result result) {
String json = getContentAsString(StandardCharsets.UTF_8);
logger.debug("JSON String: {}", json);
switch (getCommunicationStatus().getHttpCode()) {
case OK:
case ACCEPTED:
onCompleteCodeOk(json);
break;
default:
onCompleteCodeDefault(json);
}
}
/**
* handling of result in case of HTTP response OK.
*
* @param json
*/
protected void onCompleteCodeOk(@Nullable String json) {
JsonObject jsonObject = transform(json);
if (jsonObject != null) {
logger.debug("success");
handler.updateChannelStatus(transformer.transform(jsonObject, getChannelGroup()));
processResult(jsonObject);
}
}
/**
* handling of result in default case, this means error handling of http codes where no specific handling applies.
*
* @param json
*/
protected void onCompleteCodeDefault(@Nullable String json) {
JsonObject jsonObject = transform(json);
if (jsonObject == null) {
jsonObject = new JsonObject();
}
if (processFailureResponse == ProcessFailureResponse.YES) {
processResult(jsonObject);
} else {
logger.info("command failed, url: {} - code: {} - result: {}", getURL(),
getCommunicationStatus().getHttpCode(), jsonObject.get(EaseeBindingConstants.JSON_KEY_ERROR_TITLE));
}
if (retryOnFailure == RetryOnFailure.YES && retries++ < MAX_RETRIES) {
handler.enqueueCommand(this);
}
}
/**
* error safe json transformer.
*
* @param json
* @return
*/
private @Nullable JsonObject transform(@Nullable String json) {
if (json != null) {
try {
return gson.fromJson(json, JsonObject.class);
} catch (Exception ex) {
logger.debug("JSON could not be parsed: {}\nError: {}", json, ex.getMessage());
}
}
return null;
}
/**
* preparation of the request. will call a hook (prepareRequest) that has to be implemented in the subclass to add
* content to the request.
*
* @throws ValidationException
*/
@Override
public void performAction(HttpClient asyncclient, String accessToken) throws ValidationException {
Request request = asyncclient.newRequest(getURL()).timeout(handler.getBridgeConfiguration().getAsyncTimeout(),
TimeUnit.SECONDS);
// we want to send and receive json only, so explicitely set this!
request.header(HttpHeader.ACCEPT, "application/json");
request.header(HttpHeader.CONTENT_TYPE, "application/json");
// this should be the default for Easee Cloud API
request.followRedirects(false);
// add authentication data for every request. Handling this here makes it obsolete to implement for each and
// every command
if (!accessToken.isBlank()) {
request.header(HttpHeader.AUTHORIZATION, WEB_REQUEST_BEARER_TOKEN_PREFIX + accessToken);
}
prepareRequest(request).send(this);
}
/**
* @return returns Http Status Code
*/
public CommunicationStatus getCommunicationStatus() {
return communicationStatus;
}
/**
* calls the registered resultPRocessors.
*
* @param jsonObject
*/
protected final void processResult(JsonObject jsonObject) {
for (JsonResultProcessor processor : resultProcessors) {
try {
processor.processResult(getCommunicationStatus(), jsonObject);
} catch (Exception ex) {
// this should not happen
logger.warn("Exception caught: {}", ex.getMessage(), ex);
}
}
}
/**
* concrete implementation has to prepare the requests with additional parameters, etc
*
* @param requestToPrepare the request to prepare
* @return prepared Request object
* @throws ValidationException
*/
protected abstract Request prepareRequest(Request requestToPrepare) throws ValidationException;
/**
* concrete implementation has to provide the channel group.
*
* @return
*/
protected abstract String getChannelGroup();
/**
* concrete implementation has to provide the URL
*
* @return Url
*/
protected abstract String getURL();
@Override
public void registerResultProcessor(JsonResultProcessor resultProcessor) {
this.resultProcessors.add(resultProcessor);
}
}

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.CHANNEL_GROUP_NONE;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* base class for all write commands. common logic should be implemented here
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public abstract class AbstractWriteCommand extends AbstractCommand {
private final Logger logger = LoggerFactory.getLogger(AbstractWriteCommand.class);
protected final Channel channel;
protected Command command;
/**
* the constructor
*
* @param config
*/
public AbstractWriteCommand(EaseeThingHandler handler, Channel channel, Command command,
RetryOnFailure retryOnFailure, ProcessFailureResponse processFailureResponse) {
super(handler, retryOnFailure, processFailureResponse);
this.channel = channel;
this.command = command;
}
/**
* helper method for write commands that extracts value from command.
*
* @return value as String without unit.
*/
protected String getCommandValue() {
if (command instanceof QuantityType<?>) {
// this is necessary because we must not send the unit to the backend
return String.valueOf(((QuantityType<?>) command).doubleValue());
} else if (command instanceof OnOffType) {
// this is necessary because we must send booleans and not ON/OFF to the backend
return String.valueOf(command.equals(OnOffType.ON));
} else {
return command.toString();
}
}
/**
* helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
*
* @return converted JSON string
* @throws ValidationException
*/
protected String getJsonContent() throws ValidationException {
Map<String, String> content = new HashMap<String, String>(1);
content.put(channel.getUID().getIdWithoutGroup(), getCommandValue());
return gson.toJson(content);
}
@Override
protected Request prepareRequest(Request requestToPrepare) throws ValidationException {
String channelId = channel.getUID().getIdWithoutGroup();
String expr = Utils.getValidationExpression(channel);
String value = getCommandValue();
// quantity types are transformed to double and thus we might have decimals which could cause validation error.
// So we will shorten here in case no decimals are needed.
if (value.endsWith(".0")) {
value = value.substring(0, value.length() - 2);
}
if (value.matches(expr)) {
return prepareWriteRequest(requestToPrepare);
} else {
logger.info("channel '{}' does not allow value '{}' - validation rule '{}'", channelId, value, expr);
throw new ValidationException("channel (" + channelId + ") could not be updated due to a validation error");
}
}
@Override
protected String getChannelGroup() {
// this is a pure write command, thus no channel group needed.
return CHANNEL_GROUP_NONE;
}
/**
* concrete implementation has to prepare the write requests with additional parameters, etc
*
* @param requestToPrepare the request to prepare
* @return prepared Request object
* @throws ValidationException
*/
protected abstract Request prepareWriteRequest(Request requestToPrepare) throws ValidationException;
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Response.CompleteListener;
import org.eclipse.jetty.client.api.Response.ContentListener;
import org.eclipse.jetty.client.api.Response.FailureListener;
import org.eclipse.jetty.client.api.Response.SuccessListener;
import org.openhab.binding.easee.internal.model.ValidationException;
/**
* public interface for all commands
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public interface EaseeCommand extends SuccessListener, FailureListener, ContentListener, CompleteListener {
static final int MAX_RETRIES = 5;
/**
* this method is to be called by the UplinkWebinterface class
*
* @param asyncclient
* @throws ValidationException
*/
void performAction(HttpClient asyncclient, String token) throws ValidationException;
/**
* sets a result processor for json result data
*
* @param resultProcessor
*/
public void registerResultProcessor(JsonResultProcessor resultProcessor);
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.connector.CommunicationStatus;
import com.google.gson.JsonObject;
/**
* functional interface that is intended to provide a function for surther result processing of json data retrieved by a
* command.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
@FunctionalInterface
public interface JsonResultProcessor {
void processResult(CommunicationStatus status, JsonObject jsonObject);
}

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.account;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.nio.charset.StandardCharsets;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeBridgeHandler;
import com.google.gson.JsonObject;
/**
* implements the login to the webinterface
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class Login extends AbstractCommand {
class LoginData {
final String userName;
final String password;
public LoginData(String userName, String password) {
this.userName = userName;
this.password = password;
}
}
private final LoginData loginData;
public Login(EaseeBridgeHandler handler) {
// flags do not matter as "onComplete" is overwritten in this class.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.NO);
loginData = new LoginData(handler.getBridgeConfiguration().getUsername(),
handler.getBridgeConfiguration().getPassword());
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
StringContentProvider cp = new StringContentProvider(gson.toJson(loginData));
requestToPrepare.content(cp);
requestToPrepare.method(HttpMethod.POST);
return requestToPrepare;
}
@Override
protected String getURL() {
return LOGIN_URL;
}
@Override
public void onComplete(@Nullable Result result) {
String json = getContentAsString(StandardCharsets.UTF_8);
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
if (jsonObject != null) {
processResult(jsonObject);
}
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_NONE;
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.account;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.REFRESH_TOKEN_URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.handler.EaseeBridgeHandler;
/**
* implements the refresh of the access token.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class RefreshToken extends Login {
class RefreshData {
final String accessToken;
final String refreshToken;
public RefreshData(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
private final RefreshData refreshData;
public RefreshToken(EaseeBridgeHandler handler, String accessToken, String refreshToken) {
super(handler);
refreshData = new RefreshData(accessToken, refreshToken);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
StringContentProvider cp = new StringContentProvider(gson.toJson(refreshData));
requestToPrepare.content(cp);
requestToPrepare.method(HttpMethod.POST);
return requestToPrepare;
}
@Override
protected String getURL() {
return REFRESH_TOKEN_URL;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.CHANGE_CONFIGURATION_URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractWriteCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the settings api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class ChangeConfiguration extends AbstractWriteCommand {
private final String url;
public ChangeConfiguration(EaseeThingHandler handler, String chargerId, Channel channel, Command command) {
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES);
this.url = CHANGE_CONFIGURATION_URL.replaceAll("\\{id\\}", chargerId);
}
@Override
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
StringContentProvider cp = new StringContentProvider(getJsonContent());
requestToPrepare.content(cp);
requestToPrepare.method(HttpMethod.POST);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.command.JsonResultProcessor;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the charger api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class Charger extends AbstractCommand {
private final String url;
public Charger(EaseeThingHandler handler, String chargerId, JsonResultProcessor resultProcessor) {
// retry does not make much sense as it is a polling command, command should always succeed therefore update
// handler on failure.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
this.url = CHARGER_URL.replaceAll("\\{id\\}", chargerId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CHARGER;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the state api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class ChargerState extends AbstractCommand {
private final String url;
public ChargerState(EaseeThingHandler handler, String chargerId) {
// retry does not make much sense as it is a polling command, command should always succeed therefore update
// handler on failure.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES);
this.url = STATE_URL.replaceAll("\\{id\\}", chargerId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CHARGER_STATE;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the get configuration api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class GetConfiguration extends AbstractCommand {
private final String url;
public GetConfiguration(EaseeThingHandler handler, String chargerId) {
// retry does not make much sense as it is a polling command, command should always succeed therefore update
// handler on failure.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES);
this.url = GET_CONFIGURATION_URL.replaceAll("\\{id\\}", chargerId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CHARGER_CONFIG;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the latest charging session api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class LatestChargingSession extends AbstractCommand {
private final String url;
public LatestChargingSession(EaseeThingHandler handler, String chargerId) {
// retry does not make much sense as it is a polling command, command might fail if no charging sessions are
// available, therefore just ignore failure.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.NO);
this.url = LATEST_CHARGING_SESSION_URL.replaceAll("\\{id\\}", chargerId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CHARGER_LATEST_SESSION;
}
}

View File

@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.COMMANDS_URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractWriteCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the command api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SendCommand extends AbstractWriteCommand {
String url = COMMANDS_URL;
/**
* general constructor.
*
* @param handler the ThingHandler which is responsible for the channel
* @param chargerId Id of the charger which is adressed by this command
* @param channel the channel that triggered this command
* @param command the command to be send
*/
public SendCommand(EaseeThingHandler handler, String chargerId, Channel channel, Command command) {
this(handler, channel, command);
this.url = COMMANDS_URL.replaceAll("\\{id\\}", chargerId).replaceAll("\\{command\\}", getCommandValue());
}
/**
* this constructor should only be used by other command implementations that inherit this class.
*
* @param handler the ThingHandler which is responsible for the channel
* @param channel the channel that triggered this command
* @param command the command to be send
*/
SendCommand(EaseeThingHandler handler, Channel channel, Command command) {
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES);
}
@Override
protected Request prepareWriteRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.POST);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the command api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SendCommandPauseResume extends SendCommand {
public SendCommandPauseResume(EaseeThingHandler handler, String chargerId, Channel channel, Command command) {
super(handler, channel, command);
String value;
if (command.equals(OnOffType.ON)) {
value = CMD_VAL_PAUSE_CHARGING;
} else {
value = CMD_VAL_RESUME_CHARGING;
}
this.url = COMMANDS_URL.replaceAll("\\{id\\}", chargerId).replaceAll("\\{command\\}", value);
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.charger;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the command api call of the charger.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SendCommandStartStop extends SendCommand {
public SendCommandStartStop(EaseeThingHandler handler, String chargerId, Channel channel, Command command) {
super(handler, channel, command);
String value;
if (command.equals(OnOffType.ON)) {
value = CMD_VAL_START_CHARGING;
} else {
value = CMD_VAL_STOP_CHARGING;
}
this.url = COMMANDS_URL.replaceAll("\\{id\\}", chargerId).replaceAll("\\{command\\}", value);
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the settings api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class CircuitSettings extends AbstractCommand {
private final String url;
public CircuitSettings(EaseeThingHandler handler, String circuitId) {
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES);
String siteId = handler.getBridgeConfiguration().getSiteId();
this.url = CIRCUIT_SETTINGS_URL.replaceAll("\\{siteId\\}", siteId).replaceAll("\\{circuitId\\}", circuitId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CIRCUIT_SETTINGS;
}
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
/**
* implements the dynamicCurrent api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class DynamicCircuitCurrent extends AbstractCommand {
private final String url;
public DynamicCircuitCurrent(EaseeThingHandler handler, String circuitId) {
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES);
String siteId = handler.getBridgeConfiguration().getSiteId();
this.url = DYNAMIC_CIRCUIT_CURRENT_URL.replaceAll("\\{siteId\\}", siteId).replaceAll("\\{circuitId\\}",
circuitId);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_CIRCUIT_DYNAMIC_CURRENT;
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.CIRCUIT_SETTINGS_URL;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractWriteCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the settings api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SetCircuitSettings extends AbstractWriteCommand {
private final String url;
public SetCircuitSettings(EaseeThingHandler handler, Channel channel, Command command, String circuitId) {
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES);
String siteId = handler.getBridgeConfiguration().getSiteId();
this.url = CIRCUIT_SETTINGS_URL.replaceAll("\\{siteId\\}", siteId).replaceAll("\\{circuitId\\}", circuitId);
}
@Override
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
requestToPrepare.method(HttpMethod.POST);
StringContentProvider cp = new StringContentProvider(getJsonContent());
requestToPrepare.content(cp);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
}

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.DYNAMIC_CIRCUIT_CURRENT_URL;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractWriteCommand;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the dynamicCurrent api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SetDynamicCircuitCurrents extends AbstractWriteCommand {
private static final String PHASE1 = "phase1";
private static final String PHASE2 = "phase2";
private static final String PHASE3 = "phase3";
private final String url;
public SetDynamicCircuitCurrents(EaseeThingHandler handler, Channel channel, Command command, String circuitId) {
super(handler, channel, command, RetryOnFailure.YES, ProcessFailureResponse.YES);
String siteId = handler.getBridgeConfiguration().getSiteId();
this.url = DYNAMIC_CIRCUIT_CURRENT_URL.replaceAll("\\{siteId\\}", siteId).replaceAll("\\{circuitId\\}",
circuitId);
}
/**
* helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
*
* @return converted JSON string
* @throws ValidationException
*/
@Override
protected String getJsonContent() throws ValidationException {
Map<String, String> content = new HashMap<String, String>(3);
String rawCommand = getCommandValue();
String[] tokens = rawCommand.split(";");
if (tokens.length == 3) {
content.put(PHASE1, tokens[0]);
content.put(PHASE2, tokens[1]);
content.put(PHASE3, tokens[2]);
} else {
throw new ValidationException(
"malformed command string, expected: '<phase1>;<phase2>;<phase3>', actual: " + rawCommand);
}
return gson.toJson(content);
}
@Override
protected Request prepareWriteRequest(Request requestToPrepare) throws ValidationException {
requestToPrepare.method(HttpMethod.POST);
StringContentProvider cp = new StringContentProvider(getJsonContent());
requestToPrepare.content(cp);
return requestToPrepare;
}
@Override
protected String getURL() {
return url;
}
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the settings api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SetMaxCircuitCurrents extends SetCircuitSettings {
private static final String PHASE1 = "maxCircuitCurrentP1";
private static final String PHASE2 = "maxCircuitCurrentP2";
private static final String PHASE3 = "maxCircuitCurrentP3";
public SetMaxCircuitCurrents(EaseeThingHandler handler, Channel channel, Command command, String circuitId) {
super(handler, channel, command, circuitId);
}
/**
* helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
*
* @return converted JSON string
* @throws ValidationException
*/
@Override
protected String getJsonContent() throws ValidationException {
Map<String, String> content = new HashMap<String, String>(3);
String rawCommand = getCommandValue();
String[] tokens = rawCommand.split(";");
if (tokens.length == 3) {
content.put(PHASE1, tokens[0]);
content.put(PHASE2, tokens[1]);
content.put(PHASE3, tokens[2]);
} else {
throw new ValidationException(
"malformed command string, expected: '<phase1>;<phase2>;<phase3>', actual: " + rawCommand);
}
return gson.toJson(content);
}
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.circuit;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.Command;
/**
* implements the settings api call of the circuit.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class SetOfflineMaxCircuitCurrents extends SetCircuitSettings {
private static final String PHASE1 = "offlineMaxCircuitCurrentP1";
private static final String PHASE2 = "offlineMaxCircuitCurrentP2";
private static final String PHASE3 = "offlineMaxCircuitCurrentP3";
public SetOfflineMaxCircuitCurrents(EaseeThingHandler handler, Channel channel, Command command, String circuitId) {
super(handler, channel, command, circuitId);
}
/**
* helper that transforms channelId + commandvalue in a JSON string that can be added as content to a POST request.
*
* @return converted JSON string
* @throws ValidationException
*/
@Override
protected String getJsonContent() throws ValidationException {
Map<String, String> content = new HashMap<String, String>(3);
String rawCommand = getCommandValue();
String[] tokens = rawCommand.split(";");
if (tokens.length == 3) {
content.put(PHASE1, tokens[0]);
content.put(PHASE2, tokens[1]);
content.put(PHASE3, tokens[2]);
} else {
throw new ValidationException(
"malformed command string, expected: '<phase1>;<phase2>;<phase3>', actual: " + rawCommand);
}
return gson.toJson(content);
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.command.site;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.easee.internal.command.AbstractCommand;
import org.openhab.binding.easee.internal.command.JsonResultProcessor;
import org.openhab.binding.easee.internal.handler.EaseeBridgeHandler;
/**
* implements the get sites api call of the site.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class GetSite extends AbstractCommand {
public GetSite(EaseeBridgeHandler handler, JsonResultProcessor resultProcessor) {
// retry does not make much sense as it is a polling command, command should always succeed therefore update
// handler on failure.
super(handler, RetryOnFailure.NO, ProcessFailureResponse.YES, resultProcessor);
}
@Override
protected Request prepareRequest(Request requestToPrepare) {
requestToPrepare.method(HttpMethod.GET);
return requestToPrepare;
}
@Override
protected String getURL() {
String url = GET_SITE_URL;
url = url.replaceAll("\\{siteId\\}", handler.getBridgeConfiguration().getSiteId());
return url;
}
@Override
protected String getChannelGroup() {
return CHANNEL_GROUP_SITE_INFO;
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link EaseeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Alexander Friese - Initial contribution
*/
@NonNullByDefault
public class EaseeConfiguration {
private String username = "";
private String password = "";
private String siteId = "";
private Integer asyncTimeout = 120;
private Integer syncTimeout = 120;
private Integer dataPollingInterval = 120;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSiteId() {
return siteId;
}
public void setSiteId(String siteId) {
this.siteId = siteId;
}
public Integer getAsyncTimeout() {
return asyncTimeout;
}
public void setAsyncTimeout(Integer asyncTimeout) {
this.asyncTimeout = asyncTimeout;
}
public Integer getSyncTimeout() {
return syncTimeout;
}
public void setSyncTimeout(Integer syncTimeout) {
this.syncTimeout = syncTimeout;
}
public Integer getDataPollingInterval() {
return dataPollingInterval;
}
public void setDataPollingInterval(Integer dataPollingInterval) {
this.dataPollingInterval = dataPollingInterval;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("EaseeConfiguration [username=").append(username).append(", password=").append(password)
.append(", siteId=").append(siteId).append(", asyncTimeout=").append(asyncTimeout)
.append(", syncTimeout=").append(syncTimeout).append(", dataPollingInterval=")
.append(dataPollingInterval).append("]");
return builder.toString();
}
}

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.connector;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpStatus.Code;
/**
* this class contains the HTTP status of the communication and an optional exception that might occoured during
* communication
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class CommunicationStatus {
private @Nullable Code httpCode;
private @Nullable Exception error;
public final Code getHttpCode() {
Code code = httpCode;
return code == null ? Code.INTERNAL_SERVER_ERROR : code;
}
public final void setHttpCode(Code httpCode) {
this.httpCode = httpCode;
}
public final @Nullable Exception getError() {
return error;
}
public final void setError(Exception error) {
this.error = error;
}
public final String getMessage() {
Exception err = error;
String errMsg = err == null ? null : err.getMessage();
String msg = getHttpCode().getMessage();
if (errMsg != null && !errMsg.isEmpty()) {
return errMsg;
} else if (msg != null && !msg.isEmpty()) {
return msg;
}
return "";
}
}

View File

@ -0,0 +1,346 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.connector;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Queue;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.openhab.binding.easee.internal.AtomicReferenceTrait;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.EaseeCommand;
import org.openhab.binding.easee.internal.command.account.Login;
import org.openhab.binding.easee.internal.command.account.RefreshToken;
import org.openhab.binding.easee.internal.handler.EaseeBridgeHandler;
import org.openhab.binding.easee.internal.handler.StatusHandler;
import org.openhab.binding.easee.internal.model.ValidationException;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
/**
* The connector is responsible for communication with the Easee Cloud API
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class WebInterface implements AtomicReferenceTrait {
private final Logger logger = LoggerFactory.getLogger(WebInterface.class);
/**
* bridge handler
*/
private final EaseeBridgeHandler handler;
/**
* handler for updating bridge status
*/
private final StatusHandler statusHandler;
/**
* holds authentication status
*/
private boolean authenticated = false;
/**
* access token returned by login, needed to authenticate all requests send to API.
*/
private String accessToken;
/**
* refresh token returned by login, needed for refreshing the access token.
*/
private String refreshToken;
/**
* expiry of the access token.
*/
private Instant tokenExpiry;
/**
* last refresh of the access token.
*/
private Instant tokenRefreshDate;
/**
* HTTP client for asynchronous calls
*/
private final HttpClient httpClient;
/**
* the scheduler which periodically sends web requests to the Easee Cloud API. Should be initiated with the thing's
* existing scheduler instance.
*/
private final ScheduledExecutorService scheduler;
/**
* request executor
*/
private final WebRequestExecutor requestExecutor;
/**
* periodic request executor job
*/
private final AtomicReference<@Nullable Future<?>> requestExecutorJobReference;
/**
* this class is responsible for executing periodic web requests. This ensures that only one request is executed
* at the same time and there will be a guaranteed minimum delay between subsequent requests.
*
* @author afriese - initial contribution
*/
private class WebRequestExecutor implements Runnable {
/**
* queue which holds the commands to execute
*/
private final Queue<EaseeCommand> commandQueue;
/**
* constructor
*/
WebRequestExecutor() {
this.commandQueue = new BlockingArrayQueue<>(WEB_REQUEST_QUEUE_MAX_SIZE);
}
private void processAuthenticationResult(CommunicationStatus status, JsonObject jsonObject) {
String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR_TITLE);
if (msg == null || msg.isBlank()) {
msg = status.getMessage();
}
switch (status.getHttpCode()) {
case BAD_REQUEST:
statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
setAuthenticated(false);
break;
case OK:
String accessToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_ACCESS_TOKEN);
String refreshToken = Utils.getAsString(jsonObject, JSON_KEY_AUTH_REFRESH_TOKEN);
int expiresInSeconds = Utils.getAsInt(jsonObject, JSON_KEY_AUTH_EXPIRES_IN);
if (accessToken != null && refreshToken != null && expiresInSeconds != 0) {
WebInterface.this.accessToken = accessToken;
WebInterface.this.refreshToken = refreshToken;
tokenRefreshDate = Instant.now();
tokenExpiry = tokenRefreshDate.plusSeconds(expiresInSeconds);
logger.debug("access token refreshed: {}, expiry: {}", Utils.formatDate(tokenRefreshDate),
Utils.formatDate(tokenExpiry));
statusHandler.updateStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE,
STATUS_TOKEN_VALIDATED);
setAuthenticated(true);
handler.startDiscovery();
break;
}
default:
statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
setAuthenticated(false);
}
}
/**
* puts a command into the queue
*
* @param command
*/
void enqueue(EaseeCommand command) {
try {
commandQueue.add(command);
} catch (IllegalStateException ex) {
if (commandQueue.size() >= WEB_REQUEST_QUEUE_MAX_SIZE) {
logger.debug(
"Could not add command to command queue because queue is already full. Maybe Easee Cloud is down?");
} else {
logger.warn("Could not add command to queue - IllegalStateException");
}
}
}
/**
* executes the web request
*/
@Override
public void run() {
logger.debug("run queued commands, queue size is {}", commandQueue.size());
if (!isAuthenticated()) {
authenticate();
} else {
refreshAccessToken();
if (isAuthenticated() && !commandQueue.isEmpty()) {
try {
executeCommand();
} catch (Exception ex) {
logger.warn("command execution ended with exception:", ex);
}
}
}
}
/**
* authenticates with the Easee Cloud interface.
*/
private synchronized void authenticate() {
setAuthenticated(false);
EaseeCommand loginCommand = new Login(handler);
loginCommand.registerResultProcessor(this::processAuthenticationResult);
try {
loginCommand.performAction(httpClient, accessToken);
} catch (ValidationException e) {
// this cannot happen
}
}
/**
* periodically refreshed the access token.
*/
private synchronized void refreshAccessToken() {
Instant now = Instant.now();
if (now.isAfter(tokenExpiry.minus(WEB_REQUEST_TOKEN_EXPIRY_BUFFER_MINUTES, ChronoUnit.MINUTES))
|| now.isAfter(tokenRefreshDate.plus(WEB_REQUEST_TOKEN_MAX_AGE_MINUTES, ChronoUnit.MINUTES))) {
logger.debug("access token needs to be refreshed, last refresh: {}, expiry: {}",
Utils.formatDate(tokenRefreshDate), Utils.formatDate(tokenExpiry));
EaseeCommand refreshCommand = new RefreshToken(handler, accessToken, refreshToken);
refreshCommand.registerResultProcessor(this::processAuthenticationResult);
try {
refreshCommand.performAction(httpClient, accessToken);
} catch (ValidationException e) {
// this cannot happen
}
}
}
/**
* executes the next command in the queue. requires authenticated session.
*
* @throws ValidationException
*/
private void executeCommand() throws ValidationException {
EaseeCommand command = commandQueue.poll();
if (command != null) {
command.registerResultProcessor(this::processExecutionResult);
command.performAction(httpClient, accessToken);
}
}
private void processExecutionResult(CommunicationStatus status, JsonObject jsonObject) {
String msg = Utils.getAsString(jsonObject, JSON_KEY_ERROR_TITLE);
if (msg == null || msg.isBlank()) {
msg = status.getMessage();
}
switch (status.getHttpCode()) {
case OK:
case ACCEPTED:
// no action needed as the thing is already online.
break;
default:
statusHandler.updateStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msg);
setAuthenticated(false);
}
}
}
/**
* Constructor to set up interface
*
* @param config Bridge configuration
*/
public WebInterface(ScheduledExecutorService scheduler, EaseeBridgeHandler handler, HttpClient httpClient,
StatusHandler statusHandler) {
this.handler = handler;
this.statusHandler = statusHandler;
this.scheduler = scheduler;
this.httpClient = httpClient;
this.tokenExpiry = OUTDATED_DATE;
this.tokenRefreshDate = OUTDATED_DATE;
this.accessToken = "";
this.refreshToken = "";
this.requestExecutor = new WebRequestExecutor();
this.requestExecutorJobReference = new AtomicReference<>(null);
}
public void start() {
setAuthenticated(false);
updateJobReference(requestExecutorJobReference, scheduler.scheduleWithFixedDelay(requestExecutor,
WEB_REQUEST_INITIAL_DELAY, WEB_REQUEST_INTERVAL, TimeUnit.SECONDS));
}
/**
* queues any command for execution
*
* @param command
*/
public void enqueueCommand(EaseeCommand command) {
requestExecutor.enqueue(command);
}
/**
* will be called by the ThingHandler to abort periodic jobs.
*/
public void dispose() {
logger.debug("Webinterface disposed.");
cancelJobReference(requestExecutorJobReference);
setAuthenticated(false);
}
/**
* returns authentication status.
*
* @return
*/
private boolean isAuthenticated() {
return authenticated;
}
/**
* update the authentication status, also resets token data.
*
* @param authenticated
*/
private void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
if (!authenticated) {
this.tokenExpiry = OUTDATED_DATE;
this.accessToken = "";
this.refreshToken = "";
}
}
/**
* returns the current access token.
*
* @return
*/
public String getAccessToken() {
return accessToken;
}
}

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.discovery;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.easee.internal.EaseeBindingConstants;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.site.GetSite;
import org.openhab.binding.easee.internal.connector.CommunicationStatus;
import org.openhab.binding.easee.internal.handler.EaseeSiteHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* this class will handle discovery of wallboxes and circuits within the site configured.
*
* @author Alexander Friese - initial contribution
*
*/
@NonNullByDefault
public class EaseeSiteDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(EaseeSiteDiscoveryService.class);
private @NonNullByDefault({}) EaseeSiteHandler bridgeHandler;
public EaseeSiteDiscoveryService() throws IllegalArgumentException {
super(EaseeBindingConstants.SUPPORTED_THING_TYPES_UIDS, 300, false);
}
@Override
protected void startScan() {
bridgeHandler.enqueueCommand(new GetSite(bridgeHandler, this::processSiteDiscoveryResult));
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof EaseeSiteHandler) {
this.bridgeHandler = (EaseeSiteHandler) handler;
this.bridgeHandler.setDiscoveryService(this);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return bridgeHandler;
}
// method is defined in both implemented interface and inherited class, thus we must define a behaviour here.
@Override
public void deactivate() {
super.deactivate();
}
/**
* callback that handles json result data to provide discovery result.
*
* @param site
*/
private void processSiteDiscoveryResult(CommunicationStatus status, JsonObject site) {
logger.debug("processDiscoveryResult {}", site);
JsonArray circuits = site.getAsJsonArray(JSON_KEY_CIRCUITS);
if (circuits == null) {
logger.info("Site discovery failed, no circuits found.");
} else {
circuits.forEach(this::handleCircuitDiscovery);
}
}
/**
* handles each circuit discovery result.
*
* @param circuit
*/
private void handleCircuitDiscovery(JsonElement json) {
logger.debug("handleCircuitDiscovery {}", json);
JsonObject circuit = json.getAsJsonObject();
final String circuitId = Utils.getAsString(circuit, JSON_KEY_GENERIC_ID);
final String circuitName = Utils.getAsString(circuit, JSON_KEY_CIRCUIT_NAME);
if (circuitId != null) {
final String circuitLabel = circuitName != null ? circuitName : circuitId;
// handle contained chargers
JsonArray chargers = circuit.getAsJsonArray(JSON_KEY_CHARGERS);
if (chargers == null) {
logger.info("Site discovery failed, no chargers found.");
} else {
chargers.forEach(charger -> handleChargerDiscovery(charger, circuitId, circuitLabel));
}
}
}
/**
* handles each charger discovery result.
*
* @param charger
*/
private void handleChargerDiscovery(JsonElement json, String circuitId, String circuitLabel) {
logger.debug("handleChargerDiscovery {}", json);
JsonObject charger = json.getAsJsonObject();
String chargerId = Utils.getAsString(charger, JSON_KEY_GENERIC_ID);
String backPlateId = Utils.getAsString(charger.getAsJsonObject(JSON_KEY_BACK_PLATE), JSON_KEY_GENERIC_ID);
String masterBackPlateId = Utils.getAsString(charger.getAsJsonObject(JSON_KEY_BACK_PLATE),
JSON_KEY_MASTER_BACK_PLATE_ID);
String chargerName = Utils.getAsString(charger, JSON_KEY_GENERIC_NAME);
if (chargerId != null && backPlateId != null && masterBackPlateId != null) {
DiscoveryResultBuilder builder;
if (backPlateId.equals(masterBackPlateId)) {
builder = initDiscoveryResultBuilder(DEVICE_MASTER_CHARGER, chargerId, chargerName)
.withProperty(THING_CONFIG_IS_MASTER, GENERIC_YES);
} else {
builder = initDiscoveryResultBuilder(DEVICE_CHARGER, chargerId, chargerName)
.withProperty(THING_CONFIG_IS_MASTER, GENERIC_NO);
}
builder.withProperty(THING_CONFIG_CIRCUIT_ID, circuitId);
builder.withProperty(THING_CONFIG_CIRCUIT_NAME, circuitLabel);
builder.withProperty(THING_CONFIG_BACK_PLATE_ID, backPlateId);
builder.withProperty(THING_CONFIG_MASTER_BACK_PLATE_ID, masterBackPlateId);
thingDiscovered(builder.build());
}
}
/**
* sends discovery notification to the framework.
*
* @param deviceType
* @param deviceId
* @param deviceName
*/
private DiscoveryResultBuilder initDiscoveryResultBuilder(String deviceType, String deviceId,
@Nullable String deviceName) {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
ThingTypeUID typeUid = new ThingTypeUID(BINDING_ID, deviceType);
ThingUID thingUID = new ThingUID(typeUid, bridgeUID, deviceId);
String label = deviceName != null ? deviceName : deviceId;
return DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID).withLabel(label)
.withProperty(THING_CONFIG_ID, deviceId).withRepresentationProperty(THING_CONFIG_ID);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
/**
* this interface provides all methods which deal with channels
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public interface ChannelProvider {
@Nullable
Channel getChannel(String groupId, String channelId);
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BridgeHandler;
/**
* public interface of the {@link EaseeBridgeHandler}
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public interface EaseeBridgeHandler extends BridgeHandler, EaseeThingHandler {
/**
* starts discovery of wallboxes and circuits
*/
void startDiscovery();
}

View File

@ -0,0 +1,234 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.easee.internal.AtomicReferenceTrait;
import org.openhab.binding.easee.internal.EaseeBindingConstants;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.EaseeCommand;
import org.openhab.binding.easee.internal.command.charger.ChangeConfiguration;
import org.openhab.binding.easee.internal.command.charger.Charger;
import org.openhab.binding.easee.internal.command.charger.ChargerState;
import org.openhab.binding.easee.internal.command.charger.GetConfiguration;
import org.openhab.binding.easee.internal.command.charger.LatestChargingSession;
import org.openhab.binding.easee.internal.command.charger.SendCommand;
import org.openhab.binding.easee.internal.command.charger.SendCommandStartStop;
import org.openhab.binding.easee.internal.config.EaseeConfiguration;
import org.openhab.binding.easee.internal.connector.CommunicationStatus;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
/**
* The {@link EaseeChargerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class EaseeChargerHandler extends BaseThingHandler implements EaseeThingHandler, AtomicReferenceTrait {
private final Logger logger = LoggerFactory.getLogger(EaseeChargerHandler.class);
/**
* Schedule for polling live data
*/
private final AtomicReference<@Nullable Future<?>> dataPollingJobReference;
public EaseeChargerHandler(Thing thing) {
super(thing);
this.dataPollingJobReference = new AtomicReference<>(null);
}
@Override
public void initialize() {
logger.debug("About to initialize Charger");
String chargerId = getConfig().get(EaseeBindingConstants.THING_CONFIG_ID).toString();
logger.debug("Easee Charger initialized with id: {}", chargerId);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_BRIDGE);
startPolling();
enqueueCommand(new Charger(this, chargerId, this::updateProperties));
}
private void updateProperties(CommunicationStatus status, JsonObject charger) {
Map<String, String> properties = editProperties();
String backPlateId = Utils.getAsString(charger.getAsJsonObject(JSON_KEY_BACK_PLATE), JSON_KEY_GENERIC_ID);
String masterBackPlateId = Utils.getAsString(charger.getAsJsonObject(JSON_KEY_BACK_PLATE),
JSON_KEY_MASTER_BACK_PLATE_ID);
if (backPlateId != null && masterBackPlateId != null) {
if (backPlateId.equals(masterBackPlateId)) {
properties.put(THING_CONFIG_IS_MASTER, GENERIC_YES);
} else {
properties.put(THING_CONFIG_IS_MASTER, GENERIC_NO);
}
properties.put(THING_CONFIG_BACK_PLATE_ID, backPlateId);
properties.put(THING_CONFIG_MASTER_BACK_PLATE_ID, masterBackPlateId);
}
String chargerName = Utils.getAsString(charger, JSON_KEY_GENERIC_NAME);
if (chargerName != null) {
properties.put(JSON_KEY_GENERIC_NAME, chargerName);
}
String circuitId = Utils.getAsString(charger.getAsJsonObject(JSON_KEY_BACK_PLATE), JSON_KEY_CIRCUIT_ID);
if (circuitId != null) {
properties.put(JSON_KEY_CIRCUIT_ID, circuitId);
}
updateProperties(properties);
}
/**
* Start the polling.
*/
private void startPolling() {
updateJobReference(dataPollingJobReference, scheduler.scheduleWithFixedDelay(this::pollingRun,
POLLING_INITIAL_DELAY, getBridgeConfiguration().getDataPollingInterval(), TimeUnit.SECONDS));
}
/**
* Poll the Easee Cloud API one time.
*/
void pollingRun() {
String chargerId = getConfig().get(EaseeBindingConstants.THING_CONFIG_ID).toString();
logger.debug("polling charger data for {}", chargerId);
ChargerState state = new ChargerState(this, chargerId);
state.registerResultProcessor(this::updateStatusInfo);
enqueueCommand(state);
// proceed if charger is online
if (getThing().getStatus() == ThingStatus.ONLINE) {
enqueueCommand(new GetConfiguration(this, chargerId));
enqueueCommand(new LatestChargingSession(this, chargerId));
}
}
/**
* updates status depending on online information received from the API.
*
* @param status
* @param jsonObject
*/
private void updateStatusInfo(CommunicationStatus status, JsonObject jsonObject) {
Boolean isOnline = Utils.getAsBool(jsonObject, JSON_KEY_ONLINE);
if (isOnline == null) {
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, STATUS_NO_VALID_DATA);
} else if (isOnline) {
super.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
} else {
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, STATUS_NO_CONNECTION);
}
}
/**
* Disposes the thing.
*/
@Override
public void dispose() {
logger.debug("Handler disposed.");
cancelJobReference(dataPollingJobReference);
}
/**
* will update all channels provided in the map
*/
@Override
public void updateChannelStatus(Map<Channel, State> values) {
logger.debug("Handling charger channel update.");
for (Channel channel : values.keySet()) {
if (getThing().getChannels().contains(channel)) {
State value = values.get(channel);
if (value != null) {
logger.debug("Channel is to be updated: {}: {}", channel.getUID().getAsString(), value);
updateState(channel.getUID(), value);
} else {
logger.debug("Value is null or not provided by Easee Cloud (channel: {})",
channel.getUID().getAsString());
updateState(channel.getUID(), UnDefType.UNDEF);
}
} else {
logger.debug("Could not identify channel: {} for model {}", channel.getUID().getAsString(),
getThing().getThingTypeUID().getAsString());
}
}
}
@Override
public void enqueueCommand(EaseeCommand command) {
EaseeBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
bridgeHandler.enqueueCommand(command);
} else {
// this should not happen
logger.warn("no bridge handler found");
}
}
private @Nullable EaseeBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
return bridge == null ? null : ((EaseeBridgeHandler) bridge.getHandler());
}
@Override
public EaseeConfiguration getBridgeConfiguration() {
EaseeBridgeHandler bridgeHandler = getBridgeHandler();
return bridgeHandler == null ? new EaseeConfiguration() : bridgeHandler.getBridgeConfiguration();
}
@Override
public EaseeCommand buildEaseeCommand(Command command, Channel channel) {
String chargerId = getConfig().get(EaseeBindingConstants.THING_CONFIG_ID).toString();
switch (Utils.getWriteCommand(channel)) {
case COMMAND_CHANGE_CONFIGURATION:
return new ChangeConfiguration(this, chargerId, channel, command);
case COMMAND_SEND_COMMAND:
return new SendCommand(this, chargerId, channel, command);
case COMMAND_SEND_COMMAND_START_STOP:
return new SendCommandStartStop(this, chargerId, channel, command);
default:
// this should not happen
logger.error("write command '{}' not found for channel '{}'", command.toString(),
channel.getUID().getIdWithoutGroup());
throw new UnsupportedOperationException(
"write command not found for channel: " + channel.getUID().getIdWithoutGroup());
}
}
@Override
public Logger getLogger() {
return logger;
}
}

View File

@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.EaseeBindingConstants;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.EaseeCommand;
import org.openhab.binding.easee.internal.command.circuit.CircuitSettings;
import org.openhab.binding.easee.internal.command.circuit.DynamicCircuitCurrent;
import org.openhab.binding.easee.internal.command.circuit.SetCircuitSettings;
import org.openhab.binding.easee.internal.command.circuit.SetDynamicCircuitCurrents;
import org.openhab.binding.easee.internal.command.circuit.SetMaxCircuitCurrents;
import org.openhab.binding.easee.internal.command.circuit.SetOfflineMaxCircuitCurrents;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EaseeMasterChargerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class EaseeMasterChargerHandler extends EaseeChargerHandler {
private final Logger logger = LoggerFactory.getLogger(EaseeMasterChargerHandler.class);
public EaseeMasterChargerHandler(Thing thing) {
super(thing);
}
/**
* Poll the Easee Cloud API one time.
*/
@Override
void pollingRun() {
super.pollingRun();
// proceed if charger is online, otherwise circuit data is not in sync.
if (getThing().getStatus() == ThingStatus.ONLINE) {
String circuitId = getConfig().get(EaseeBindingConstants.THING_CONFIG_CIRCUIT_ID).toString();
logger.debug("polling circuit data for {}", circuitId);
enqueueCommand(new DynamicCircuitCurrent(this, circuitId));
enqueueCommand(new CircuitSettings(this, circuitId));
}
}
@Override
public EaseeCommand buildEaseeCommand(Command command, Channel channel) {
String circuitId = getConfig().get(EaseeBindingConstants.THING_CONFIG_CIRCUIT_ID).toString();
switch (Utils.getWriteCommand(channel)) {
case COMMAND_SET_CIRCUIT_SETTINGS:
return new SetCircuitSettings(this, channel, command, circuitId);
case COMMAND_SET_DYNAMIC_CIRCUIT_CURRENTS:
return new SetDynamicCircuitCurrents(this, channel, command, circuitId);
case COMMAND_SET_MAX_CIRCUIT_CURRENTS:
return new SetMaxCircuitCurrents(this, channel, command, circuitId);
case COMMAND_SET_OFFLINE_MAX_CIRCUIT_CURRENTS:
return new SetOfflineMaxCircuitCurrents(this, channel, command, circuitId);
default:
return super.buildEaseeCommand(command, channel);
}
}
}

View File

@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.EaseeCommand;
import org.openhab.binding.easee.internal.command.site.GetSite;
import org.openhab.binding.easee.internal.config.EaseeConfiguration;
import org.openhab.binding.easee.internal.connector.CommunicationStatus;
import org.openhab.binding.easee.internal.connector.WebInterface;
import org.openhab.binding.easee.internal.discovery.EaseeSiteDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
/**
* The {@link EaseeSiteHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class EaseeSiteHandler extends BaseBridgeHandler implements EaseeBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(EaseeSiteHandler.class);
private @Nullable DiscoveryService discoveryService;
/**
* Interface object for querying the Easee web interface
*/
private WebInterface webInterface;
public EaseeSiteHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.webInterface = new WebInterface(scheduler, this, httpClient, super::updateStatus);
}
@Override
public void initialize() {
logger.debug("About to initialize Easee Site");
EaseeConfiguration config = getBridgeConfiguration();
logger.debug("Easee Site initialized with configuration: {}", config.toString());
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, STATUS_WAITING_FOR_LOGIN);
webInterface.start();
enqueueCommand(new GetSite(this, this::updateProperties));
}
private void updateProperties(CommunicationStatus status, JsonObject jsonObject) {
Map<String, String> properties = editProperties();
String name = Utils.getAsString(jsonObject, JSON_KEY_GENERIC_NAME);
if (name != null) {
properties.put(JSON_KEY_GENERIC_NAME, name);
}
String siteKey = Utils.getAsString(jsonObject, JSON_KEY_SITE_KEY);
if (siteKey != null) {
properties.put(JSON_KEY_SITE_KEY, siteKey);
}
updateProperties(properties);
}
/**
* Disposes the bridge.
*/
@Override
public void dispose() {
logger.debug("Handler disposed.");
webInterface.dispose();
}
@Override
public EaseeConfiguration getBridgeConfiguration() {
return this.getConfigAs(EaseeConfiguration.class);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(EaseeSiteDiscoveryService.class);
}
public void setDiscoveryService(EaseeSiteDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
@Override
public void startDiscovery() {
DiscoveryService discoveryService = this.discoveryService;
if (discoveryService != null) {
discoveryService.startScan(null);
}
}
@Override
public void enqueueCommand(EaseeCommand command) {
webInterface.enqueueCommand(command);
}
@Override
public Logger getLogger() {
return logger;
}
}

View File

@ -0,0 +1,136 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.CHANNEL_TYPEPREFIX_RW;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.command.EaseeCommand;
import org.openhab.binding.easee.internal.config.EaseeConfiguration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
/**
* public interface of the {@link EaseeThingHandler}. provides some default implementations which can be used by both
* BridgeHandlers and ThingHandlers.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public interface EaseeThingHandler extends ThingHandler, ChannelProvider {
/**
* just to avoid usage of static loggers.
*
* @return
*/
Logger getLogger();
/**
* method which updates the channels. needs to be implemented by thing types which have channels.
*
* @param values key-value list where key is the channel
*/
default void updateChannelStatus(Map<Channel, State> values) {
getLogger().debug("updateChannelStatus not implemented/supported by this thing type");
}
/**
* return the bridge's configuration
*
* @return
*/
EaseeConfiguration getBridgeConfiguration();
/**
* passes a new command o the command queue
*
* @param command to be queued/executed
*/
void enqueueCommand(EaseeCommand command);
/**
* default implementation to handle commands
*
* @param channelUID
* @param command
*/
@Override
default void handleCommand(ChannelUID channelUID, Command command) {
getLogger().debug("command for {}: {}", channelUID, command);
if (command instanceof RefreshType) {
return;
}
String group = channelUID.getGroupId();
group = group == null ? "" : group;
Channel channel = getChannel(group, channelUID.getIdWithoutGroup());
if (channel == null) {
// this should not happen
getLogger().warn("channel not found: {}", channelUID);
return;
}
String channelType = Utils.getChannelTypeId(channel);
if (!channelType.startsWith(CHANNEL_TYPEPREFIX_RW)) {
getLogger().info("channel '{}' does not support write access - value to set '{}'",
channelUID.getIdWithoutGroup(), command);
throw new UnsupportedOperationException(
"channel (" + channelUID.getIdWithoutGroup() + ") does not support write access");
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
getLogger().debug("Thing is not online, thus no commands will be handled");
return;
}
enqueueCommand(buildEaseeCommand(command, channel));
}
/**
* builds the EaseeCommand which can be send to the webinterface.
*
* @param command the openhab raw command received from the framework
* @param channel the channel which belongs to the command.
* @return
*/
default EaseeCommand buildEaseeCommand(Command command, Channel channel) {
throw new UnsupportedOperationException("buildEaseeCommand not implemented/supported by this thing type");
}
/**
* determines the channel for a given groupId and channelId.
*
* @param groupId groupId of the channel
* @param channelId channelId of the channel
*/
@Override
default @Nullable Channel getChannel(String groupId, String channelId) {
ThingUID thingUID = this.getThing().getUID();
ChannelGroupUID channelGroupUID = new ChannelGroupUID(thingUID, groupId);
Channel channel = getThing().getChannel(new ChannelUID(channelGroupUID, channelId));
return channel;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
/**
* functional interface to provide an function to update status of a thing or bridge.
*
* @author Alexander Friese - initial contribution
*/
@FunctionalInterface
@NonNullByDefault
public interface StatusHandler {
/**
* Called from WebInterface#authenticate() to update
* the thing status because updateStatus is protected.
*
* @param status Thing status
* @param statusDetail Thing status detail
* @param description Thing status description
*/
void updateStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, String description);
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* exception whichs is used to state a validation error
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class ConfigurationException extends RuntimeException {
private static final long serialVersionUID = 5736865225953884234L;
public ConfigurationException() {
super();
}
public ConfigurationException(String message) {
super(message);
}
public ConfigurationException(Throwable cause) {
super(cause);
}
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.model;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.handler.ChannelProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.State;
import com.google.gson.JsonObject;
/**
* transforms the http response into the openhab datamodel (instances of State).
* This class is used to handle special cases which cannot be mapped by the generic transformer.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
class CustomResponseTransformer {
private final ChannelProvider channelProvider;
CustomResponseTransformer(ChannelProvider channelProvider) {
this.channelProvider = channelProvider;
}
/**
* allows additional updates of special/composite channels.
*
* @param triggerChannel the channel which triggers the additional update
* @param value updated value of the triggering channel
* @param rawData raw json data provided by the API
*/
Map<Channel, State> transform(Channel triggerChannel, String value, JsonObject rawData) {
Map<Channel, State> result = new HashMap<>(20);
switch (triggerChannel.getUID().getId()) {
case CHANNEL_GROUP_CHARGER_STATE + "#" + CHANNEL_CHARGER_OP_MODE:
updateChargerStartStop(result, value, rawData);
break;
case CHANNEL_GROUP_CHARGER_STATE + "#" + CHANNEL_CHARGER_DYNAMIC_CURRENT:
updateChargerPauseResume(result, value);
break;
case CHANNEL_GROUP_CIRCUIT_DYNAMIC_CURRENT + "#" + CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE1:
updateCompositePhaseChannel(result, rawData, CHANNEL_GROUP_CIRCUIT_DYNAMIC_CURRENT,
CHANNEL_CIRCUIT_DYNAMIC_CURRENTS, CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE1,
CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE2, CHANNEL_CIRCUIT_DYNAMIC_CURRENT_PHASE3);
break;
case CHANNEL_GROUP_CIRCUIT_SETTINGS + "#" + CHANNEL_CIRCUIT_MAX_CURRENT_PHASE1:
updateCompositePhaseChannel(result, rawData, CHANNEL_GROUP_CIRCUIT_SETTINGS,
CHANNEL_CIRCUIT_MAX_CURRENTS, CHANNEL_CIRCUIT_MAX_CURRENT_PHASE1,
CHANNEL_CIRCUIT_MAX_CURRENT_PHASE2, CHANNEL_CIRCUIT_MAX_CURRENT_PHASE3);
break;
case CHANNEL_GROUP_CIRCUIT_SETTINGS + "#" + CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE1:
updateCompositePhaseChannel(result, rawData, CHANNEL_GROUP_CIRCUIT_SETTINGS,
CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENTS, CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE1,
CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE2, CHANNEL_CIRCUIT_OFFLINE_MAX_CURRENT_PHASE3);
break;
}
return result;
}
private void updateChargerStartStop(Map<Channel, State> result, String value, JsonObject rawData) {
Channel channel = channelProvider.getChannel(CHANNEL_GROUP_CHARGER_COMMANDS, CHANNEL_CHARGER_START_STOP);
if (channel != null) {
int val = Integer.valueOf(value);
// state >= 3 will mean charging, ready to charge or charging finished
boolean charging = val >= CHARGER_OP_STATE_CHARGING;
String rfnc = Utils.getAsString(rawData, CHANNEL_CHARGER_REASON_FOR_NO_CURRENT);
int reasonForNoCurrent = Integer.valueOf(rfnc == null ? "-1" : rfnc);
boolean paused = (val == CHARGER_OP_STATE_WAITING
&& reasonForNoCurrent == CHARGER_REASON_FOR_NO_CURRENT_PAUSED);
result.put(channel, OnOffType.from(charging || paused));
}
}
private void updateChargerPauseResume(Map<Channel, State> result, String value) {
Channel channel = channelProvider.getChannel(CHANNEL_GROUP_CHARGER_COMMANDS, CHANNEL_CHARGER_PAUSE_RESUME);
if (channel != null) {
int val = Integer.valueOf(value);
// value == 0 will mean paused
boolean paused = val == CHARGER_DYNAMIC_CURRENT_PAUSE;
result.put(channel, OnOffType.from(paused));
}
}
private void updateCompositePhaseChannel(Map<Channel, State> result, JsonObject rawData, final String group,
final String targetChannel, final String channelPhase1, final String channelPhase2,
final String channelPhase3) {
Channel channel = channelProvider.getChannel(group, targetChannel);
String phase1 = Utils.getAsString(rawData, channelPhase1);
String phase2 = Utils.getAsString(rawData, channelPhase2);
String phase3 = Utils.getAsString(rawData, channelPhase3);
if (channel != null && phase1 != null && phase2 != null && phase3 != null) {
phase1 = phase1.replaceAll("\\.0", "");
phase2 = phase2.replaceAll("\\.0", "");
phase3 = phase3.replaceAll("\\.0", "");
result.put(channel, new StringType(phase1 + ";" + phase2 + ";" + phase3));
}
}
}

View File

@ -0,0 +1,127 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.model;
import static org.openhab.binding.easee.internal.EaseeBindingConstants.*;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Map;
import javax.measure.MetricPrefix;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.easee.internal.Utils;
import org.openhab.binding.easee.internal.handler.ChannelProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
/**
* transforms the http response into the openhab datamodel (instances of State)
* this is a generic trnasformer which tries to map json fields 1:1 to channels.
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class GenericResponseTransformer {
private final Logger logger = LoggerFactory.getLogger(GenericResponseTransformer.class);
private final ChannelProvider channelProvider;
private final CustomResponseTransformer customResponseTransformer;
public GenericResponseTransformer(ChannelProvider channelProvider) {
this.channelProvider = channelProvider;
this.customResponseTransformer = new CustomResponseTransformer(channelProvider);
}
public Map<Channel, State> transform(JsonObject jsonData, String group) {
Map<Channel, State> result = new HashMap<>(20);
for (String channelId : jsonData.keySet()) {
String value = Utils.getAsString(jsonData, channelId);
Channel channel = channelProvider.getChannel(group, channelId);
if (channel == null) {
// As we have a generic response mapper it ould happen that a subset of key/values in the response
// cannot be mapped to openhab channels.
logger.debug("Channel not found: {}#{}", group, channelId);
} else {
logger.debug("mapping value '{}' to channel {}", value, channel.getUID().getId());
String channelType = channel.getAcceptedItemType();
if (value == null || channelType == null) {
result.put(channel, UnDefType.NULL);
} else {
try {
switch (channelType) {
case CHANNEL_TYPE_SWITCH:
result.put(channel, OnOffType.from(Boolean.parseBoolean(value)));
break;
case CHANNEL_TYPE_VOLT:
result.put(channel, new QuantityType<>(Double.parseDouble(value), Units.VOLT));
break;
case CHANNEL_TYPE_AMPERE:
result.put(channel, new QuantityType<>(Double.parseDouble(value), Units.AMPERE));
break;
case CHANNEL_TYPE_KWH:
result.put(channel, new QuantityType<>(Double.parseDouble(value),
MetricPrefix.KILO(Units.WATT_HOUR)));
break;
case CHANNEL_TYPE_KW:
result.put(channel,
new QuantityType<>(Double.parseDouble(value), MetricPrefix.KILO(Units.WATT)));
break;
case CHANNEL_TYPE_DATE:
result.put(channel, new DateTimeType(Utils.parseDate(value)));
break;
case CHANNEL_TYPE_STRING:
result.put(channel, new StringType(value));
break;
case CHANNEL_TYPE_NUMBER:
if (Utils.getChannelTypeId(channel).contains(CHANNEL_TYPENAME_INTEGER)) {
// explicit type long is needed in case of integer/long values otherwise automatic
// transformation to a decimal type is applied.
result.put(channel, new DecimalType(Long.parseLong(value)));
} else {
result.put(channel, new DecimalType(Double.parseDouble(value)));
}
break;
default:
logger.warn("no mapping implemented for channel type '{}'", channelType);
}
// call the custom handler to handle specific / composite channels which do not map 1:1 to JSON
// fields.
result.putAll(customResponseTransformer.transform(channel, value, jsonData));
} catch (NumberFormatException | DateTimeParseException ex) {
logger.warn("caught exception while parsing data for channel {} (value '{}'). Exception: {}",
channel.getUID().getId(), value, ex.getMessage());
}
}
}
}
return result;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.easee.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* exception whichs is used to state a validation error
*
* @author Alexander Friese - initial contribution
*/
@NonNullByDefault
public class ValidationException extends Exception {
private static final long serialVersionUID = -6479556472780307224L;
public ValidationException() {
super();
}
public ValidationException(String message) {
super(message);
}
public ValidationException(Throwable cause) {
super(cause);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="easee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Easee Binding</name>
<description>This is the binding for Easee Wallboxes.</description>
</binding:binding>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:easee:site">
<parameter-group name="authentication">
<label>Authentication</label>
<description>Authentication settings.</description>
</parameter-group>
<parameter-group name="connection">
<label>Connection</label>
<description>Connection settings.</description>
</parameter-group>
<parameter-group name="general">
<label>General</label>
<description>General settings.</description>
</parameter-group>
<parameter name="username" type="text" required="true" groupName="authentication">
<label>Username</label>
<description>The username to login at Easee Cloud service. This should be an e-mail address or phone number.</description>
</parameter>
<parameter name="password" type="text" required="true" groupName="authentication">
<label>Password</label>
<description>Your password to login at Easee Cloud service.</description>
<context>password</context>
</parameter>
<parameter name="siteId" type="text" required="true" groupName="general">
<label>Site ID</label>
<description>The ID of the site containing the charger(s) and circuit(s) that should be integrated into openHAB.</description>
</parameter>
<parameter name="dataPollingInterval" type="integer" required="false" min="20" max="600" unit="s"
groupName="connection">
<label>Polling Interval</label>
<description>Interval in which data is polled from EaseeCloud (in seconds).</description>
<default>60</default>
</parameter>
</config-description>
<config-description uri="thing-type:easee:charger">
<parameter name="id" type="text" required="true">
<label>ID</label>
<description>The ID of the charger that should be integrated into openHAB.</description>
</parameter>
</config-description>
<config-description uri="thing-type:easee:mastercharger">
<parameter name="id" type="text" required="true">
<label>ID</label>
<description>The ID of the charger that should be integrated into openHAB.</description>
</parameter>
<parameter name="circuitId" type="text" required="true">
<label>Circuit ID</label>
<description>The ID of the circuit which is controlled by this charger.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,167 @@
# binding
binding.easee.name = Easee Binding
binding.easee.description = This is the binding for Easee Wallboxes.
# thing types
thing-type.easee.charger.label = Easee Charger
thing-type.easee.charger.description = Cloud connected Easee Charger.
thing-type.easee.mastercharger.label = Easee Master Charger
thing-type.easee.mastercharger.description = Cloud connected Easee Master Charger which also controls the circuit.
thing-type.easee.site.label = Easee Site
thing-type.easee.site.description = Cloud connection to an Easee site.
# thing types config
thing-type.config.easee.charger.id.label = ID
thing-type.config.easee.charger.id.description = The ID of the charger that should be integrated into openHAB.
thing-type.config.easee.mastercharger.circuitId.label = Circuit ID
thing-type.config.easee.mastercharger.circuitId.description = The ID of the circuit which is controlled by this charger.
thing-type.config.easee.mastercharger.id.label = ID
thing-type.config.easee.mastercharger.id.description = The ID of the charger that should be integrated into openHAB.
thing-type.config.easee.site.dataPollingInterval.label = Polling Interval
thing-type.config.easee.site.dataPollingInterval.description = Interval in which data is polled from EaseeCloud (in seconds).
thing-type.config.easee.site.group.authentication.label = Authentication
thing-type.config.easee.site.group.authentication.description = Authentication settings.
thing-type.config.easee.site.group.connection.label = Connection
thing-type.config.easee.site.group.connection.description = Connection settings.
thing-type.config.easee.site.group.general.label = General
thing-type.config.easee.site.group.general.description = General settings.
thing-type.config.easee.site.password.label = Password
thing-type.config.easee.site.password.description = Your password to login at Easee Cloud service.
thing-type.config.easee.site.siteId.label = Site ID
thing-type.config.easee.site.siteId.description = The ID of the site containing the charger(s) and circuit(s) that should be integrated into openHAB.
thing-type.config.easee.site.username.label = Username
thing-type.config.easee.site.username.description = The username to login at Easee Cloud service. This should be an e-mail address or phone number.
# channel group types
channel-group-type.easee.charger-commands.label = Charger Commands
channel-group-type.easee.charger-commands.channel.genericCommand.label = Generic Charger Command
channel-group-type.easee.charger-commands.channel.genericCommand.description = Sends a command to the charger. Write only channel.
channel-group-type.easee.charger-commands.channel.pauseResume.label = Pause/Resume Charging
channel-group-type.easee.charger-commands.channel.pauseResume.description = Pauses/Resumes charging. Pausing works by setting Dynamic Charger Current to 0.
channel-group-type.easee.charger-commands.channel.startStop.label = Start/Stop Charging
channel-group-type.easee.charger-commands.channel.startStop.description = Starts/Stops charging. Only relevant if authorization is required.
channel-group-type.easee.charger-config.label = Charger Configuration
channel-group-type.easee.charger-config.channel.authorizationRequired.label = Authorization Required
channel-group-type.easee.charger-config.channel.authorizationRequired.description = Indicates if authorization is required to start charging.
channel-group-type.easee.charger-config.channel.limitToSinglePhaseCharging.label = Limit To Single Phase Charging
channel-group-type.easee.charger-config.channel.limitToSinglePhaseCharging.description = Indicates if charging should be limited to single phase mode.
channel-group-type.easee.charger-config.channel.lockCablePermanently.label = Lock Cable Permanently
channel-group-type.easee.charger-config.channel.lockCablePermanently.description = Lock Cable Permanently status of the wallbox.
channel-group-type.easee.charger-config.channel.maxChargerCurrent.label = Max Charger Current
channel-group-type.easee.charger-config.channel.maxChargerCurrent.description = Max charging current.
channel-group-type.easee.charger-config.channel.phaseMode.label = Phase Mode
channel-group-type.easee.charger-config.channel.phaseMode.description = Phase Mode.
channel-group-type.easee.charger-latestSession.label = Last Charging Session
channel-group-type.easee.charger-latestSession.channel.sessionEnd.label = Session End
channel-group-type.easee.charger-latestSession.channel.sessionEnd.description = Date/Time when session ended.
channel-group-type.easee.charger-latestSession.channel.sessionEnergy.label = Total Session Energy
channel-group-type.easee.charger-latestSession.channel.sessionEnergy.description = Total Energy for last session.
channel-group-type.easee.charger-latestSession.channel.sessionStart.label = Session Start
channel-group-type.easee.charger-latestSession.channel.sessionStart.description = Date/Time when session started.
channel-group-type.easee.charger-state.label = Charger Status
channel-group-type.easee.charger-state.channel.cableLocked.label = Cable Locked
channel-group-type.easee.charger-state.channel.cableLocked.description = Cable Locked status of the wallbox.
channel-group-type.easee.charger-state.channel.chargerFirmware.label = Current Firmware
channel-group-type.easee.charger-state.channel.chargerFirmware.description = Current Firmware of the wallbox.
channel-group-type.easee.charger-state.channel.chargerOpMode.label = Charger Operation Mode
channel-group-type.easee.charger-state.channel.chargerOpMode.description = Current operation mode.
channel-group-type.easee.charger-state.channel.dynamicChargerCurrent.label = Dynamic Charger Current
channel-group-type.easee.charger-state.channel.dynamicChargerCurrent.description = Dynamic set charging current.
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP1.label = Dynamic Circuit Current P1
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP1.description = Dynamic set circuit current for phase 1.
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP2.label = Dynamic Circuit Current P2
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP2.description = Dynamic set circuit current for phase 2.
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP3.label = Dynamic Circuit Current P3
channel-group-type.easee.charger-state.channel.dynamicCircuitCurrentP3.description = Dynamic set circuit current for phase 3.
channel-group-type.easee.charger-state.channel.errorCode.label = Error Code
channel-group-type.easee.charger-state.channel.errorCode.description = Error Code.
channel-group-type.easee.charger-state.channel.fatalErrorCode.label = Fatal Error Code
channel-group-type.easee.charger-state.channel.fatalErrorCode.description = Fatal Error Code.
channel-group-type.easee.charger-state.channel.isOnline.label = Online
channel-group-type.easee.charger-state.channel.isOnline.description = Online status of the wallbox.
channel-group-type.easee.charger-state.channel.latestFirmware.label = Latest Firmware
channel-group-type.easee.charger-state.channel.latestFirmware.description = Latest Firmware which is available for the wallbox.
channel-group-type.easee.charger-state.channel.latestPulse.label = Latest Pulse
channel-group-type.easee.charger-state.channel.latestPulse.description = Last data received from charger.
channel-group-type.easee.charger-state.channel.lifetimeEnergy.label = Lifetime Energy
channel-group-type.easee.charger-state.channel.lifetimeEnergy.description = Total lifetime energy.
channel-group-type.easee.charger-state.channel.outputCurrent.label = Output Current
channel-group-type.easee.charger-state.channel.outputCurrent.description = Actual charging current.
channel-group-type.easee.charger-state.channel.reasonForNoCurrent.label = Reason for no Current
channel-group-type.easee.charger-state.channel.reasonForNoCurrent.description = Reason for no Current.
channel-group-type.easee.charger-state.channel.sessionEnergy.label = Session Energy
channel-group-type.easee.charger-state.channel.sessionEnergy.description = Energy for current session.
channel-group-type.easee.charger-state.channel.smartCharging.label = Smart Charging
channel-group-type.easee.charger-state.channel.smartCharging.description = Smart Charging status of the wallbox.
channel-group-type.easee.charger-state.channel.totalPower.label = Total Power
channel-group-type.easee.charger-state.channel.totalPower.description = Total Power for all active phases.
channel-group-type.easee.charger-state.channel.voltage.label = Voltage
channel-group-type.easee.charger-state.channel.voltage.description = Voltage
channel-group-type.easee.circuit-dynamicCurrent.label = Circuit Dynamic Current
channel-group-type.easee.circuit-dynamicCurrent.channel.dynamicCurrents.label = Dynamic Currents
channel-group-type.easee.circuit-dynamicCurrent.channel.dynamicCurrents.description = Dynamic circuit currents for phases 1,2,3.
channel-group-type.easee.circuit-dynamicCurrent.channel.phase1.label = Dynamic Current P1
channel-group-type.easee.circuit-dynamicCurrent.channel.phase1.description = Dynamic set circuit current for phase 1.
channel-group-type.easee.circuit-dynamicCurrent.channel.phase2.label = Dynamic Current P2
channel-group-type.easee.circuit-dynamicCurrent.channel.phase2.description = Dynamic set circuit current for phase 2.
channel-group-type.easee.circuit-dynamicCurrent.channel.phase3.label = Dynamic Current P3
channel-group-type.easee.circuit-dynamicCurrent.channel.phase3.description = Dynamic set circuit current for phase 3.
channel-group-type.easee.circuit-settings.label = Circuit Settings
channel-group-type.easee.circuit-settings.channel.allowOfflineMaxCircuitCurrent.label = Allow Offline Max Circuit Current
channel-group-type.easee.circuit-settings.channel.allowOfflineMaxCircuitCurrent.description = Allow Offline Max Circuit Current.
channel-group-type.easee.circuit-settings.channel.enableIdleCurrent.label = Enable Idle Current
channel-group-type.easee.circuit-settings.channel.enableIdleCurrent.description = This will block 6A Idle current for each charger in the current.
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP1.label = Max Current P1
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP1.description = Max circuit current for phase 1.
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP2.label = Max Current P2
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP2.description = Max circuit current for phase 2.
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP3.label = Max Current P3
channel-group-type.easee.circuit-settings.channel.maxCircuitCurrentP3.description = Max circuit current for phase 3.
channel-group-type.easee.circuit-settings.channel.maxCurrents.label = Max Currents
channel-group-type.easee.circuit-settings.channel.maxCurrents.description = Max circuit currents for phases 1,2,3.
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP1.label = Offline Max Current P1
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP1.description = Max circuit current for phase 1 in offline mode.
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP2.label = Offline Max Current P2
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP2.description = Max circuit current for phase 2 in offline mode.
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP3.label = Offline Max Current P3
channel-group-type.easee.circuit-settings.channel.offlineMaxCircuitCurrentP3.description = Max circuit current for phase 3 in offline mode.
channel-group-type.easee.circuit-settings.channel.offlineMaxCurrents.label = Offline Max Currents
channel-group-type.easee.circuit-settings.channel.offlineMaxCurrents.description = Offline Max circuit currents for phases 1,2,3.
# channel types
channel-type.easee.rwtype-charger-command.label = Charger Command
channel-type.easee.rwtype-current.label = Electric Current
channel-type.easee.rwtype-currents.label = Currents Phase 1;2;3
channel-type.easee.rwtype-integer-phase-mode.label = Phase Mode
channel-type.easee.rwtype-integer-phase-mode.state.option.1 = 1-Phase
channel-type.easee.rwtype-integer-phase-mode.state.option.2 = Auto
channel-type.easee.rwtype-integer-phase-mode.state.option.3 = 3-Phase
channel-type.easee.rwtype-switch.label = Switch
channel-type.easee.type-current.label = ElectricCurrent
channel-type.easee.type-date.label = DateTime
channel-type.easee.type-energy.label = Energy
channel-type.easee.type-integer-charger-op-mode.label = Charger Operation Mode
channel-type.easee.type-integer-charger-op-mode.state.option.0 = Offline
channel-type.easee.type-integer-charger-op-mode.state.option.1 = Disconnected
channel-type.easee.type-integer-charger-op-mode.state.option.2 = AwaitingStart
channel-type.easee.type-integer-charger-op-mode.state.option.3 = Charging
channel-type.easee.type-integer-charger-op-mode.state.option.4 = Completed
channel-type.easee.type-integer-charger-op-mode.state.option.5 = Error
channel-type.easee.type-integer-charger-op-mode.state.option.6 = ReadyToCharge
channel-type.easee.type-integer.label = Generic Integer
channel-type.easee.type-power.label = Power
channel-type.easee.type-switch.label = Switch
channel-type.easee.type-volt.label = ElectricPotential
# status translations
status.token.validated = "Access token validated"
status.waiting.for.bridge = "Waiting for bridge to go online"
status.waiting.for.login = "Waiting for web api login"
status.no.valid.data = "No valid data received. This is most likely a configuration error"
status.no.connection = "Charger might have no internet connection or fuse tripped"

View File

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="easee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="charger-state">
<label>Charger Status</label>
<channels>
<channel id="smartCharging" typeId="type-switch">
<label>Smart Charging</label>
<description>Smart Charging status of the wallbox.</description>
</channel>
<channel id="cableLocked" typeId="type-switch">
<label>Cable Locked</label>
<description>Cable Locked status of the wallbox.</description>
</channel>
<channel id="chargerOpMode" typeId="type-integer-charger-op-mode">
<label>Charger Operation Mode</label>
<description>Current operation mode.</description>
</channel>
<channel id="totalPower" typeId="type-power">
<label>Total Power</label>
<description>Total Power for all active phases.</description>
</channel>
<channel id="sessionEnergy" typeId="type-energy">
<label>Session Energy</label>
<description>Energy for current session.</description>
</channel>
<channel id="dynamicCircuitCurrentP1" typeId="type-current">
<label>Dynamic Circuit Current P1</label>
<description>Dynamic set circuit current for phase 1.</description>
</channel>
<channel id="dynamicCircuitCurrentP2" typeId="type-current">
<label>Dynamic Circuit Current P2</label>
<description>Dynamic set circuit current for phase 2.</description>
</channel>
<channel id="dynamicCircuitCurrentP3" typeId="type-current">
<label>Dynamic Circuit Current P3</label>
<description>Dynamic set circuit current for phase 3.</description>
</channel>
<channel id="latestPulse" typeId="type-date">
<label>Latest Pulse</label>
<description>Last data received from charger.</description>
</channel>
<channel id="chargerFirmware" typeId="type-integer">
<label>Current Firmware</label>
<description>Current Firmware of the wallbox.</description>
</channel>
<channel id="latestFirmware" typeId="type-integer">
<label>Latest Firmware</label>
<description>Latest Firmware which is available for the wallbox.</description>
</channel>
<channel id="voltage" typeId="type-volt">
<label>Voltage</label>
<description>Voltage</description>
</channel>
<channel id="outputCurrent" typeId="type-current">
<label>Output Current</label>
<description>Actual charging current.</description>
</channel>
<channel id="isOnline" typeId="type-switch">
<label>Online</label>
<description>Online status of the wallbox.</description>
</channel>
<channel id="dynamicChargerCurrent" typeId="rwtype-current">
<label>Dynamic Charger Current</label>
<description>Dynamic set charging current.</description>
<properties>
<property name="writeCommand">ChangeConfiguration</property>
<property name="validationExpression">(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)</property>
</properties>
</channel>
<channel id="reasonForNoCurrent" typeId="type-integer">
<label>Reason for no Current</label>
<description>Reason for no Current.</description>
</channel>
<channel id="lifetimeEnergy" typeId="type-energy">
<label>Lifetime Energy</label>
<description>Total lifetime energy.</description>
</channel>
<channel id="errorCode" typeId="type-integer">
<label>Error Code</label>
<description>Error Code.</description>
</channel>
<channel id="fatalErrorCode" typeId="type-integer">
<label>Fatal Error Code</label>
<description>Fatal Error Code.</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="charger-config">
<label>Charger Configuration</label>
<channels>
<channel id="lockCablePermanently" typeId="rwtype-switch">
<label>Lock Cable Permanently</label>
<description>Lock Cable Permanently status of the wallbox.</description>
<properties>
<property name="writeCommand">ChangeConfiguration</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
<channel id="authorizationRequired" typeId="rwtype-switch">
<label>Authorization Required</label>
<description>Indicates if authorization is required to start charging.</description>
<properties>
<property name="writeCommand">ChangeConfiguration</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
<channel id="limitToSinglePhaseCharging" typeId="rwtype-switch">
<label>Limit To Single Phase Charging</label>
<description>Indicates if charging should be limited to single phase mode.</description>
<properties>
<property name="writeCommand">ChangeConfiguration</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
<channel id="phaseMode" typeId="rwtype-integer-phase-mode">
<label>Phase Mode</label>
<description>Phase Mode.</description>
<properties>
<property name="writeCommand">ChangeConfiguration</property>
<property name="validationExpression">(1|2|3)</property>
</properties>
</channel>
<channel id="maxChargerCurrent" typeId="type-current">
<label>Max Charger Current</label>
<description>Max charging current.</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="charger-latestSession">
<label>Last Charging Session</label>
<channels>
<channel id="sessionEnergy" typeId="type-energy">
<label>Total Session Energy</label>
<description>Total Energy for last session.</description>
</channel>
<channel id="sessionStart" typeId="type-date">
<label>Session Start</label>
<description>Date/Time when session started.</description>
</channel>
<channel id="sessionEnd" typeId="type-date">
<label>Session End</label>
<description>Date/Time when session ended.</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="charger-commands">
<label>Charger Commands</label>
<channels>
<channel id="genericCommand" typeId="rwtype-charger-command">
<label>Generic Charger Command</label>
<description>Sends a command to the charger. Write only channel.</description>
<properties>
<property name="writeCommand">SendCommand</property>
<property name="validationExpression">(reboot|update_firmware|poll_all|smart_charging|start_charging|stop_charging|pause_charging|resume_charging|toggle_charging|override_schedule)</property>
</properties>
</channel>
<channel id="startStop" typeId="rwtype-switch">
<label>Start/Stop Charging</label>
<description>Starts/Stops charging. Only relevant if authorization is required.</description>
<properties>
<property name="writeCommand">SendCommandStartStop</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
<channel id="pauseResume" typeId="rwtype-switch">
<label>Pause/Resume Charging</label>
<description>Pauses/Resumes charging. Pausing works by setting Dynamic Charger Current to 0.</description>
<properties>
<property name="writeCommand">SendCommandPauseResume</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="easee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="circuit-settings">
<label>Circuit Settings</label>
<channels>
<channel id="maxCircuitCurrentP1" typeId="type-current">
<label>Max Current P1</label>
<description>Max circuit current for phase 1.</description>
</channel>
<channel id="maxCircuitCurrentP2" typeId="type-current">
<label>Max Current P2</label>
<description>Max circuit current for phase 2.</description>
</channel>
<channel id="maxCircuitCurrentP3" typeId="type-current">
<label>Max Current P3</label>
<description>Max circuit current for phase 3.</description>
</channel>
<channel id="maxCurrents" typeId="rwtype-currents">
<label>Max Currents</label>
<description>Max circuit currents for phases 1,2,3.</description>
<properties>
<property name="writeCommand">SetMaxCircuitCurrents</property>
<property name="validationExpression">(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)</property>
</properties>
</channel>
<channel id="offlineMaxCircuitCurrentP1" typeId="type-current">
<label>Offline Max Current P1</label>
<description>Max circuit current for phase 1 in offline mode.</description>
</channel>
<channel id="offlineMaxCircuitCurrentP2" typeId="type-current">
<label>Offline Max Current P2</label>
<description>Max circuit current for phase 2 in offline mode.</description>
</channel>
<channel id="offlineMaxCircuitCurrentP3" typeId="type-current">
<label>Offline Max Current P3</label>
<description>Max circuit current for phase 3 in offline mode.</description>
</channel>
<channel id="offlineMaxCurrents" typeId="rwtype-currents">
<label>Offline Max Currents</label>
<description>Offline Max circuit currents for phases 1,2,3.</description>
<properties>
<property name="writeCommand">SetOfflineMaxCircuitCurrents</property>
<property name="validationExpression">(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)</property>
</properties>
</channel>
<channel id="enableIdleCurrent" typeId="rwtype-switch">
<label>Enable Idle Current</label>
<description>This will block 6A Idle current for each charger in the current.</description>
<properties>
<property name="writeCommand">SetCircuitSettings</property>
<property name="validationExpression">.*</property>
</properties>
</channel>
<channel id="allowOfflineMaxCircuitCurrent" typeId="type-switch">
<label>Allow Offline Max Circuit Current</label>
<description>Allow Offline Max Circuit Current.</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="circuit-dynamicCurrent">
<label>Circuit Dynamic Current</label>
<channels>
<channel id="phase1" typeId="type-current">
<label>Dynamic Current P1</label>
<description>Dynamic set circuit current for phase 1.</description>
</channel>
<channel id="phase2" typeId="type-current">
<label>Dynamic Current P2</label>
<description>Dynamic set circuit current for phase 2.</description>
</channel>
<channel id="phase3" typeId="type-current">
<label>Dynamic Current P3</label>
<description>Dynamic set circuit current for phase 3.</description>
</channel>
<channel id="dynamicCurrents" typeId="rwtype-currents">
<label>Dynamic Currents</label>
<description>Dynamic circuit currents for phases 1,2,3.</description>
<properties>
<property name="writeCommand">SetDynamicCircuitCurrents</property>
<property name="validationExpression">(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32);(0|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)</property>
</properties>
</channel>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="easee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="type-power">
<item-type>Number:Power</item-type>
<label>Power</label>
<state pattern="%.2f %unit%" readOnly="true">
</state>
</channel-type>
<channel-type id="type-energy">
<item-type>Number:Energy</item-type>
<label>Energy</label>
<state pattern="%.1f %unit%" readOnly="true">
</state>
</channel-type>
<channel-type id="type-current">
<item-type>Number:ElectricCurrent</item-type>
<label>ElectricCurrent</label>
<state pattern="%.1f %unit%" readOnly="true">
</state>
</channel-type>
<channel-type id="type-switch">
<item-type>Switch</item-type>
<label>Switch</label>
<state readOnly="true"></state>
</channel-type>
<channel-type id="type-volt">
<item-type>Number:ElectricPotential</item-type>
<label>ElectricPotential</label>
<state pattern="%.1f %unit%" readOnly="true">
</state>
</channel-type>
<channel-type id="type-integer">
<item-type>Number</item-type>
<label>Generic Integer</label>
<state pattern="%d" readOnly="true">
</state>
</channel-type>
<channel-type id="type-integer-charger-op-mode">
<item-type>Number</item-type>
<label>Charger Operation Mode</label>
<state readOnly="true">
<options>
<option value="0">Offline</option>
<option value="1">Disconnected</option>
<option value="2">AwaitingStart</option>
<option value="3">Charging</option>
<option value="4">Completed</option>
<option value="5">Error</option>
<option value="6">ReadyToCharge</option>
</options>
</state>
</channel-type>
<channel-type id="type-date">
<item-type>DateTime</item-type>
<label>DateTime</label>
<state readOnly="true">
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="easee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="rwtype-switch">
<item-type>Switch</item-type>
<label>Switch</label>
<state readOnly="false"></state>
</channel-type>
<channel-type id="rwtype-current">
<item-type>Number:ElectricCurrent</item-type>
<label>Electric Current</label>
<state pattern="%.1f %unit%" readOnly="false">
</state>
</channel-type>
<channel-type id="rwtype-integer-phase-mode">
<item-type>Number</item-type>
<label>Phase Mode</label>
<state readOnly="false">
<options>
<option value="1">1-Phase</option>
<option value="2">Auto</option>
<option value="3">3-Phase</option>
</options>
</state>
</channel-type>
<channel-type id="rwtype-currents">
<item-type>String</item-type>
<label>Currents Phase 1;2;3</label>
<state pattern="%s" readOnly="false">
</state>
</channel-type>
<channel-type id="rwtype-charger-command">
<item-type>String</item-type>
<label>Charger Command</label>
<state pattern="%s" readOnly="false">
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="easee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="site">
<label>Easee Site</label>
<description>Cloud connection to an Easee site.</description>
<config-description-ref uri="thing-type:easee:site"/>
</bridge-type>
<thing-type id="mastercharger">
<supported-bridge-type-refs>
<bridge-type-ref id="site"/>
</supported-bridge-type-refs>
<label>Easee Master Charger</label>
<description>Cloud connected Easee Master Charger which also controls the circuit.</description>
<channel-groups>
<channel-group typeId="charger-state" id="state"/>
<channel-group typeId="charger-config" id="config"/>
<channel-group typeId="charger-commands" id="commands"/>
<channel-group typeId="charger-latestSession" id="latestSession"/>
<channel-group typeId="circuit-dynamicCurrent" id="dynamicCurrent"/>
<channel-group typeId="circuit-settings" id="settings"/>
</channel-groups>
<config-description-ref uri="thing-type:easee:mastercharger"/>
</thing-type>
<thing-type id="charger">
<supported-bridge-type-refs>
<bridge-type-ref id="site"/>
</supported-bridge-type-refs>
<label>Easee Charger</label>
<description>Cloud connected Easee Charger.</description>
<channel-groups>
<channel-group typeId="charger-state" id="state"/>
<channel-group typeId="charger-config" id="config"/>
<channel-group typeId="charger-commands" id="commands"/>
<channel-group typeId="charger-latestSession" id="latestSession"/>
</channel-groups>
<config-description-ref uri="thing-type:easee:charger"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -109,6 +109,7 @@
<module>org.openhab.binding.dsmr</module>
<module>org.openhab.binding.dwdpollenflug</module>
<module>org.openhab.binding.dwdunwetter</module>
<module>org.openhab.binding.easee</module>
<module>org.openhab.binding.ecobee</module>
<module>org.openhab.binding.ecotouch</module>
<module>org.openhab.binding.ekey</module>