Compare commits

...

5 commits

25 changed files with 258 additions and 67 deletions

View file

@ -3,6 +3,30 @@ plugins {
id 'application' id 'application'
} }
/*
* The MIT License (MIT)
*
* Copyright (c) 2025 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// ------------------------------------ // ------------------------------------
// Hal common configuration // Hal common configuration
// ------------------------------------ // ------------------------------------
@ -62,7 +86,7 @@ distributions {
from 'hal.conf.example' from 'hal.conf.example'
from 'logging.properties' from 'logging.properties'
from "${buildDir}/resources" from sourceSets.main.output.resourcesDir
} }
} }
} }
@ -76,10 +100,11 @@ task copyRecources(type: Copy) {
from "${subProject.projectDir}/resources" from "${subProject.projectDir}/resources"
} }
into("${buildDir}/resources") into(sourceSets.main.output.resourcesDir)
} }
processResources.finalizedBy(copyRecources) jar.dependsOn(copyRecources)
copyRecources.mustRunAfter(processResources)
application { application {
mainClass = 'se.hal.HalServer' mainClass = 'se.hal.HalServer'

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -1,3 +1,27 @@
<!--
~ The MIT License (MIT)
~
~ Copyright (c) 2025 Ziver Koc
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->
<h1 class="page-header">Event Configuration</h1> <h1 class="page-header">Event Configuration</h1>
<div class="col-md-12"> <div class="col-md-12">
@ -23,7 +47,7 @@
{{#localEvents}} {{#localEvents}}
<tr> <tr>
<td>{{.getId()}}</td> <td>{{.getId()}}</td>
<td>{{.getName()}}</td> <td><a href="event/{{.getId()}}">{{.getName()}}</a></td>
<td>{{.getType()}}</td> <td>{{.getType()}}</td>
<td>{{.getDeviceConfig()}}</td> <td>{{.getDeviceConfig()}}</td>
<td> <td>

View file

@ -17,7 +17,7 @@ export default {
}, },
template: ` template: `
<tr :data-device-id="event.id"> <tr :data-device-id="event.id">
<td><a :href="'?id=' + event.id">{{ event.name }}</a></td> <td><a :href="'event/' + event.id">{{ event.name }}</a></td>
<td>{{ event.configType }}</td> <td>{{ event.configType }}</td>
<td>{{ event.data?.valueStr }}</td> <td>{{ event.data?.valueStr }}</td>
<td class="timestamp">{{ timestamp }}</td> <td class="timestamp">{{ timestamp }}</td>

View file

@ -1,3 +1,27 @@
<!--
~ The MIT License (MIT)
~
~ Copyright (c) 2025 Ziver Koc
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -53,14 +77,14 @@
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"vue": "./js/vue/vue.esm-browser.js", "vue": "./js/vue/lib/vue.esm-browser.js",
"vue-router": "./js/vue/vue-router.esm-browser.js", "vue-router": "./js/vue/lib/vue-router.esm-browser.js",
"@vue/devtools-api": "https://unpkg.com/@vue/devtools-api@6.4.5/lib/esm/index.js", "@vue/devtools-api": "https://unpkg.com/@vue/devtools-api@6.4.5/lib/esm/index.js",
{{#javascriptModules}}"{{.getModuleName()}}": "{{.getScriptPath()}}", {{#javascriptModules}}"{{.getModuleName()}}": "{{.getScriptPath()}}",
{{/javascriptModules}} {{/javascriptModules}}
"App": "./js/vue/components/App.js" "App": "./js/vue/App.js"
} }
} }
</script> </script>

View file

@ -1,3 +1,27 @@
<!--
~ The MIT License (MIT)
~
~ Copyright (c) 2025 Ziver Koc
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in
~ all copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
~ THE SOFTWARE.
-->
<h1 class="page-header">Sensor Configuration</h1> <h1 class="page-header">Sensor Configuration</h1>
<div class="col-md-12"> <div class="col-md-12">
@ -23,7 +47,7 @@
{{#localSensors}} {{#localSensors}}
<tr> <tr>
<td>{{.getId()}}</td> <td>{{.getId()}}</td>
<td>{{.getName()}}</td> <td><a href="sensor_overview?id={{.getId()}}">{{.getName()}}</a></td>
<td>{{.getType()}}</td> <td>{{.getType()}}</td>
<td>{{.isSynced()}}</td> <td>{{.isSynced()}}</td>
<td>{{.getDeviceConfig()}}</td> <td>{{.getDeviceConfig()}}</td>

View file

@ -1,3 +1,27 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2025 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package se.hal.intf; package se.hal.intf;
import zutil.ClassUtil; import zutil.ClassUtil;
@ -43,7 +67,7 @@ public abstract class HalAbstractControllerManager<T extends HalAbstractControll
// Instantiate autostart controllers, but only the first time // Instantiate autostart controllers, but only the first time
synchronized (autostartProcessed) { synchronized (this) {
if (!autostartProcessed) { if (!autostartProcessed) {
for (Iterator<Class<? extends HalAutostartController>> it = pluginManager.getClassIterator(HalAutostartController.class); it.hasNext(); ) { for (Iterator<Class<? extends HalAutostartController>> it = pluginManager.getClassIterator(HalAutostartController.class); it.hasNext(); ) {
Class controller = it.next(); Class controller = it.next();

View file

@ -1,3 +1,27 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2025 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package se.hal.page; package se.hal.page;
import se.hal.intf.HalJavascriptModule; import se.hal.intf.HalJavascriptModule;
@ -7,7 +31,7 @@ public class JavascriptModules implements HalJavascriptModule {
@Override @Override
public HalJsModule[] getJavascriptModules() { public HalJsModule[] getJavascriptModules() {
HalWebPage.getRootNav().createSubNav("Events").createSubNav("event_overview", "Overview"); HalWebPage.getRootNav().createSubNav("Events").createSubNav("events", "Overview");
return new HalJsModule[] { return new HalJsModule[] {
new HalJsModule("AlertStore", "./js/vue/stores/AlertStore.js"), new HalJsModule("AlertStore", "./js/vue/stores/AlertStore.js"),
@ -19,7 +43,7 @@ public class JavascriptModules implements HalJavascriptModule {
new HalJsModule("EventActionComponent", "./js/vue/components/EventActionComponent.js"), new HalJsModule("EventActionComponent", "./js/vue/components/EventActionComponent.js"),
new HalJsModulePage("EventDetailPageComponent", "./js/vue/components/EventDetailPageComponent.js", "/event/:id"), new HalJsModulePage("EventDetailPageComponent", "./js/vue/components/EventDetailPageComponent.js", "/event/:id"),
new HalJsModulePage("EventOverviewPageComponent", "./js/vue/components/EventOverviewPageComponent.js", "/event_overview"), new HalJsModulePage("EventOverviewPageComponent", "./js/vue/components/EventOverviewPageComponent.js", "/events"),
new HalJsModule("EventTableComponent", "./js/vue/components/EventTableComponent.js"), new HalJsModule("EventTableComponent", "./js/vue/components/EventTableComponent.js"),
new HalJsModule("EventTableRowComponent", "./js/vue/components/EventTableRowComponent.js"), new HalJsModule("EventTableRowComponent", "./js/vue/components/EventTableRowComponent.js"),
}; };

View file

@ -36,6 +36,10 @@ hal_core.http_port=8080
## Tibber Plugin ## Tibber Plugin
#hal_tibber.token= #hal_tibber.token=
## EcoSense plugin
#hal_vendor_ecosense.username=
#hal_vendor_ecosense.password=
## Zigbee plugin ## Zigbee plugin
#hal_zigbee.com_port=COM4 #hal_zigbee.com_port=COM4
#hal_zigbee.dongle=CC2531|CONBEE|XBEE #hal_zigbee.dongle=CC2531|CONBEE|XBEE

View file

@ -40,8 +40,11 @@ import zutil.parser.json.JSONWriter;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.text.SimpleDateFormat; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -59,8 +62,6 @@ public class EcoSenseCloudAPIClient {
private static final String USER_POOL_REGION = "us-west-2"; private static final String USER_POOL_REGION = "us-west-2";
private static final String API_URL = "https://api.cloud.ecosense.io/api"; private static final String API_URL = "https://api.cloud.ecosense.io/api";
private static SimpleDateFormat DATE_PARSER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); //2025-10-22T14:09:29.691533
private String username; private String username;
private String password; private String password;
/** The authenticated Amazon Cognito ID token. */ /** The authenticated Amazon Cognito ID token. */
@ -92,7 +93,9 @@ public class EcoSenseCloudAPIClient {
cognitoClient.close(); cognitoClient.close();
} }
public EcoSenseDevice getDevices() { public List<EcoSenseDevice> getDevices() {
List<EcoSenseDevice> devices = new ArrayList<>();
try { try {
if (ObjectUtil.isEmpty(idToken)) { if (ObjectUtil.isEmpty(idToken)) {
authenticate(); authenticate();
@ -147,13 +150,13 @@ public class EcoSenseCloudAPIClient {
ecoSenseDevice.wifiName = deviceNode.getString("wifi_name"); ecoSenseDevice.wifiName = deviceNode.getString("wifi_name");
try { try {
ecoSenseDevice.lastRadonUpdateTime = DATE_PARSER.parse(deviceNode.getString("last_radon_update_time")).getTime(); ecoSenseDevice.lastRadonUpdateTime = LocalDateTime.parse(deviceNode.getString("last_radon_update_time")).toInstant(ZoneOffset.UTC).toEpochMilli();
ecoSenseDevice.lastUpdateTime = DATE_PARSER.parse(deviceNode.getString("last_update_time")).getTime(); ecoSenseDevice.lastUpdateTime = LocalDateTime.parse(deviceNode.getString("last_update_time")).toInstant(ZoneOffset.UTC).toEpochMilli();
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.WARNING, "Was unable to parse timestamp from EcoSense API.", e); logger.log(Level.WARNING, "Was unable to parse timestamp from EcoSense API.", e);
} }
return ecoSenseDevice; devices.add(ecoSenseDevice);
} }
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -162,7 +165,7 @@ public class EcoSenseCloudAPIClient {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return null; return devices;
} }
public class EcoSenseDevice { public class EcoSenseDevice {

View file

@ -27,7 +27,7 @@ package se.hal.plugin.vendor.ecosense;
import se.hal.HalContext; import se.hal.HalContext;
import se.hal.HalServer; import se.hal.HalServer;
import se.hal.intf.*; import se.hal.intf.*;
import se.hal.plugin.vendor.ecosense.device.EccoCubeRadonSensor; import se.hal.plugin.vendor.ecosense.device.EccoCubeRadonSensorConfig;
import se.hal.struct.devicedata.RadonSensorData; import se.hal.struct.devicedata.RadonSensorData;
import zutil.ObjectUtil; import zutil.ObjectUtil;
import zutil.log.LogUtil; import zutil.log.LogUtil;
@ -77,21 +77,23 @@ public class EcoSenseController implements HalSensorController, Runnable, HalDae
public void run() { public void run() {
try { try {
if (!ObjectUtil.isEmpty(ecoSenseClient)) { if (!ObjectUtil.isEmpty(ecoSenseClient)) {
EcoSenseCloudAPIClient.EcoSenseDevice apiResponse = ecoSenseClient.getDevices(); List<EcoSenseCloudAPIClient.EcoSenseDevice> apiResponse = ecoSenseClient.getDevices();
if (apiResponse == null) { if (apiResponse.isEmpty()) {
logger.warning("Received empty response from EcoSense API."); logger.info("Received no devices from EcoSense API.");
return; return;
} }
EccoCubeRadonSensor radonSensor = new EccoCubeRadonSensor(); for (EcoSenseCloudAPIClient.EcoSenseDevice device : apiResponse) {
radonSensor.setSerialNumber(apiResponse.serialNumber); EccoCubeRadonSensorConfig radonSensor = new EccoCubeRadonSensorConfig();
radonSensor.setSerialNumber(device.serialNumber);
RadonSensorData radonSensorData = new RadonSensorData(apiResponse.radonLevel, apiResponse.lastRadonUpdateTime); RadonSensorData radonSensorData = new RadonSensorData(device.radonLevel, device.lastRadonUpdateTime);
if (deviceReportListener != null) { if (deviceReportListener != null) {
deviceReportListener.reportReceived(radonSensor, radonSensorData); deviceReportListener.reportReceived(radonSensor, radonSensorData);
} }
} }
}
} catch (Exception e) { } catch (Exception e) {
logger.log(Level.SEVERE, "Failed to poll EcoSense API.", e); logger.log(Level.SEVERE, "Failed to poll EcoSense API.", e);
} }

View file

@ -29,12 +29,16 @@ import se.hal.intf.HalSensorController;
import se.hal.intf.HalSensorData; import se.hal.intf.HalSensorData;
import se.hal.plugin.vendor.ecosense.EcoSenseController; import se.hal.plugin.vendor.ecosense.EcoSenseController;
import se.hal.struct.devicedata.RadonSensorData; import se.hal.struct.devicedata.RadonSensorData;
import zutil.ui.conf.Configurator;
import java.util.Objects;
/** /**
* A sensor that calculate current radon level * A sensor that calculate current radon level
*/ */
public class EccoCubeRadonSensor implements HalSensorConfig { public class EccoCubeRadonSensorConfig implements HalSensorConfig {
@Configurator.Configurable(value = "Device serial number")
private String serialNumber; private String serialNumber;
@ -63,7 +67,20 @@ public class EccoCubeRadonSensor implements HalSensorConfig {
} }
@Override @Override
public boolean equals(Object obj) { public final boolean equals(Object o) {
return false; if (!(o instanceof EccoCubeRadonSensorConfig)) return false;
EccoCubeRadonSensorConfig that = (EccoCubeRadonSensorConfig) o;
return Objects.equals(serialNumber, that.serialNumber);
}
@Override
public int hashCode() {
return Objects.hashCode(serialNumber);
}
@Override
public String toString() {
return "Serial number: " + serialNumber;
} }
} }

View file

@ -5,6 +5,6 @@
"interfaces": [ "interfaces": [
{"se.hal.intf.HalAutostartController": "se.hal.plugin.vendor.ecosense.EcoSenseController"}, {"se.hal.intf.HalAutostartController": "se.hal.plugin.vendor.ecosense.EcoSenseController"},
{"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.ecosense.device.EccoCubeRadonSensor"} {"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.ecosense.device.EccoCubeRadonSensorConfig"}
] ]
} }

View file

@ -1,3 +1,27 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2025 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
dependencies { dependencies {
implementation project(':hal-core') implementation project(':hal-core')
} }

View file

@ -35,6 +35,8 @@ import zutil.parser.json.JSONParser;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
@ -44,7 +46,6 @@ public class TibberAPIClient {
private static final Logger logger = LogUtil.getLogger(); private static final Logger logger = LogUtil.getLogger();
private static final String TIBBER_API_ENDPOINT = "https://api.tibber.com/v1-beta/gql"; private static final String TIBBER_API_ENDPOINT = "https://api.tibber.com/v1-beta/gql";
private static SimpleDateFormat DATE_PARSER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); // 2024-12-28T23:00:00.000+01:00
private String tibberToken; private String tibberToken;
@ -80,16 +81,16 @@ public class TibberAPIClient {
"consumptionUnit " + // Should be Kwh "consumptionUnit " + // Should be Kwh
"}" + "}" +
"}" + "}" +
"currentSubscription {" + // "currentSubscription {" +
"priceInfo {" + // "priceInfo {" +
"current {" + // "current {" +
"total " + // "total " +
"energy " + // "energy " +
"tax " + // "tax " +
"startsAt " + // "startsAt " +
"}" + // "}" +
"}" + // "}" +
"}" + // "}" +
"}" + "}" +
"}" + "}" +
"}\"" + "}\"" +
@ -100,12 +101,7 @@ public class TibberAPIClient {
// unitPrice=0.1507875, cost=0.3131856375, from=2024-12-28T21:00:00.000+01:00, consumption=2.077, to=2024-12-28T22:00:00.000+01:00, unitPriceVAT=0.0301575, consumptionUnit=kWh // unitPrice=0.1507875, cost=0.3131856375, from=2024-12-28T21:00:00.000+01:00, consumption=2.077, to=2024-12-28T22:00:00.000+01:00, unitPriceVAT=0.0301575, consumptionUnit=kWh
TibberConsumption consumption = new TibberConsumption(); TibberConsumption consumption = new TibberConsumption();
consumption.timestamp = LocalDateTime.parse(data.getString("from")).toInstant(ZoneOffset.UTC).toEpochMilli();
try {
consumption.timestamp = DATE_PARSER.parse(data.getString("from")).getTime();
} catch (ParseException e) {
logger.warning("Was unable to parse timestamp from Tibber API.");
}
if (data.get("cost") != null) { if (data.get("cost") != null) {
consumption.cost = data.getDouble("cost"); consumption.cost = data.getDouble("cost");

View file

@ -27,9 +27,9 @@ package se.hal.plugin.vendor.tibber;
import se.hal.HalContext; import se.hal.HalContext;
import se.hal.HalServer; import se.hal.HalServer;
import se.hal.intf.*; import se.hal.intf.*;
import se.hal.plugin.vendor.tibber.device.TibberElectricityCostSensor; import se.hal.plugin.vendor.tibber.device.TibberElectricityCostSensorConfig;
import se.hal.plugin.vendor.tibber.device.TibberElectricityPriceSensor; import se.hal.plugin.vendor.tibber.device.TibberElectricityPriceSensorConfig;
import se.hal.plugin.vendor.tibber.device.TibberPowerConsumptionSensor; import se.hal.plugin.vendor.tibber.device.TibberPowerConsumptionSensorConfig;
import se.hal.struct.devicedata.PriceSensorData; import se.hal.struct.devicedata.PriceSensorData;
import se.hal.util.ListenerUtil; import se.hal.util.ListenerUtil;
import zutil.log.LogUtil; import zutil.log.LogUtil;
@ -92,13 +92,13 @@ public class TibberController implements HalSensorController, Runnable, HalDaemo
} }
if (consumption.cost > 0) { if (consumption.cost > 0) {
ListenerUtil.callReportReceived(deviceListeners, new TibberElectricityCostSensor(), new PriceSensorData(consumption.cost, consumption.timestamp)); ListenerUtil.callReportReceived(deviceListeners, new TibberElectricityCostSensorConfig(), new PriceSensorData(consumption.cost, consumption.timestamp));
} }
if (consumption.unitPriceVAT > 0) { if (consumption.unitPriceVAT > 0) {
ListenerUtil.callReportReceived(deviceListeners, new TibberElectricityPriceSensor(), new PriceSensorData(consumption.unitPriceVAT, consumption.timestamp)); ListenerUtil.callReportReceived(deviceListeners, new TibberElectricityPriceSensorConfig(), new PriceSensorData(consumption.unitPriceVAT, consumption.timestamp));
} }
if (consumption.consumption > 0) { if (consumption.consumption > 0) {
ListenerUtil.callReportReceived(deviceListeners, new TibberPowerConsumptionSensor(), new PriceSensorData(consumption.consumption, consumption.timestamp)); ListenerUtil.callReportReceived(deviceListeners, new TibberPowerConsumptionSensorConfig(), new PriceSensorData(consumption.consumption, consumption.timestamp));
} }
} catch (Exception e) { } catch (Exception e) {

View file

@ -33,7 +33,7 @@ import se.hal.struct.devicedata.CostSensorData;
/** /**
* A sensor that calculate current electricity bil * A sensor that calculate current electricity bil
*/ */
public class TibberElectricityCostSensor implements HalSensorConfig { public class TibberElectricityCostSensorConfig implements HalSensorConfig {
@ -59,6 +59,6 @@ public class TibberElectricityCostSensor implements HalSensorConfig {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof TibberElectricityCostSensor; return obj instanceof TibberElectricityCostSensorConfig;
} }
} }

View file

@ -33,7 +33,7 @@ import se.hal.struct.devicedata.PriceSensorData;
/** /**
* A sensor that shows the price of electricity at a specific time * A sensor that shows the price of electricity at a specific time
*/ */
public class TibberElectricityPriceSensor implements HalSensorConfig { public class TibberElectricityPriceSensorConfig implements HalSensorConfig {
@ -59,6 +59,6 @@ public class TibberElectricityPriceSensor implements HalSensorConfig {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof TibberElectricityPriceSensor; return obj instanceof TibberElectricityPriceSensorConfig;
} }
} }

View file

@ -31,7 +31,7 @@ import se.hal.struct.devicedata.PowerConsumptionSensorData;
import se.hal.plugin.vendor.tibber.TibberController; import se.hal.plugin.vendor.tibber.TibberController;
public class TibberPowerConsumptionSensor implements HalSensorConfig { public class TibberPowerConsumptionSensorConfig implements HalSensorConfig {
@Override @Override
@ -56,6 +56,6 @@ public class TibberPowerConsumptionSensor implements HalSensorConfig {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof TibberPowerConsumptionSensor; return obj instanceof TibberPowerConsumptionSensorConfig;
} }
} }

View file

@ -5,8 +5,8 @@
"interfaces": [ "interfaces": [
{"se.hal.intf.HalAutostartController": "se.hal.plugin.vendor.tibber.TibberController"}, {"se.hal.intf.HalAutostartController": "se.hal.plugin.vendor.tibber.TibberController"},
{"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberElectricityCostSensor"}, {"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberElectricityCostSensorConfig"},
{"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberElectricityPriceSensor"}, {"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberElectricityPriceSensorConfig"},
{"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberPowerConsumptionSensor"} {"se.hal.intf.HalSensorConfig": "se.hal.plugin.vendor.tibber.device.TibberPowerConsumptionSensorConfig"}
] ]
} }