diff --git a/bundles/org.openhab.binding.tacmi/README.md b/bundles/org.openhab.binding.tacmi/README.md index b1ac7cdbd6148..98783be56831d 100644 --- a/bundles/org.openhab.binding.tacmi/README.md +++ b/bundles/org.openhab.binding.tacmi/README.md @@ -18,11 +18,37 @@ CoE (CAN over Ethernet) Connection - Send ON/OFF to digital CAN-inputs defined in TAPPS2 - Send numeric values to analog CAN-inputs defined in TAPPS2 +JSON API + +- Read selected data via the JSON API (inputs, outputs, logging). + See [API documentation](https://wiki.ta.co.at/C.M.I._JSON-API) for details + Depending on what you want to achieve, either the "Schema API Page" or the CoE way might be better. As rough guidance: Anything you want to provide to the TA equipment it has to work / operate with the CoE might be better. If you plan things mainly for user interaction the "Schema API Page" might be better. -## Prerequisites +## Supported Bridge and Things + +- TA C.M.I. **Schema API Connection** - Thing \ + This thing reflecting one of our 'schema API page' as defined in the prerequisites. + This thing doesn't need the bridge. + Multiple of these pages on different C.M.I.'s could be defined within an openHAB instance. +- TA C.M.I. **CoE Bridge** \ + In order to get the CAN over Ethernet (COE) envionment working a `coe-bridge` has to be created. + The bridge itself opens the UDP port 5441 for communication with the C.M.I. devices. + The bridge could be used for multiple C.M.I. devices. +- TA C.M.I. **CoE Connection** - Thing \ + This thing reflects a connection to a node behind a specific C.M.I.. + This node could be every CAN-Capable device from TA which allows to define a CAN-Input. +- TA C.M.I. **JSON API Connection** - Thing \ + This is a thing connection that regularly polls the CMI using the JSON API. + +## Discovery + +Autodiscovering is not supported. +You have to define the things manually. + +## Schema API ### Setting up the "Schema API Page" @@ -53,47 +79,7 @@ The first sample is a sensor reading, but also the 'operation mode' of a heating In this screenshot you also see the schema page id - the parenthesized number on the bottom page overview, in this sample 4. -### CoE Configuration - -#### Configure CAN outputs in TAPPS2 - -You need to configure CAN outputs in your Functional data on the UVR16x2. -This can be done by using the TAPPS2 application from TA. Follow the user guide on how to do this. - -#### Configure your CMI for CoE - -Now follow the User Guide of the CMI on how to setup CAN over Ethernet (COE). -Here you will map your outputs that you configured in the previous step. -This can be accomplished via the GUI on the CMI or via the coe.csv file. -As the target device you need to put the IP of your openHAB server. -Don’t forget to reboot the CMI after you uploaded the coe.csv file. - -## Supported Bridge and Things - -- TA C.M.I. schema API connection - Thing - -This thing reflecting one of our 'schema API page' as defined in the prerequisites. -This thing doesn't need the bridge. -Multiple of these pages on different C.M.I.'s could be defined within an openHAB instance. - -- TA C.M.I. CoE Bridge - -In order to get the CAN over Ethernet (COE) envionment working a `coe-bridge` has to be created. -The bridge itself opens the UDP port 5441 for communication with the C.M.I. devices. -The bridge could be used for multiple C.M.I. devices. - -- TA C.M.I. CoE Connection - Thing - -This thing reflects a connection to a node behind a specific C.M.I.. -This node could be every CAN-Capable device from TA which allows to define a CAN-Input. - -## Discovery - -Autodiscovering is not supported. We have to define the things manually. - -## Thing Configuration - -### TA C.M.I. schema API connection +### Connection Configuration The _TA C.M.I. Schema API Connection_ has to be manually configured. @@ -109,22 +95,7 @@ The thing has the following configuration parameters: This thing doesn't need a bridge. Multiple of these things for different C.M.I.'s could be defined within an openHAB instance. -### TA C.M.I. CoE Connection - -The _TA C.M.I. CoE Connection_ has to be manually configured. - -This thing reflects a connection to a node behind a specific C.M.I.. This node could be every CAN-Capable device from TA which allows to define a CAN-Input. - -| Parameter Label | Parameter ID | Description | Accepted values | -|-------------------------|-----------------|---------------------------------------------------------------------------------------------------------------|------------------------| -| C.M.I. IP Address | host | Host name or IP address of the C.M.I | host name or ip | -| Node | node | The CoE / CAN Node number openHAB should represent | 1-64 | - -The thing has no channels by default - they have to be added manually matching the configured inputs / outputs for the related CAN Node. Digital and Analog channels are supported. Please read TA's documentation related to the CAN-protocol - multiple analog (4) and digital (16) channels are combined so please be aware of this design limitation. - -## Channels - -### TA C.M.I. schema API connection +### TA C.M.I. schema API Channels The channels provided by this thing depend on the configuration of the "schema API page". All the channels are dynamically created to match it. @@ -142,8 +113,50 @@ The behavior in detail: - `On-Fetch` (`1`): This is the default for read-only values. This means the channel is updated every time the schema page is polled. Ideally for values you want to monitor and log into charts. - `On-Change` (`2`): When channel values can be changed via OH it is better to only update the channel when the value changes. The binding will cache the previous value and only send an update when it changes to the previous known value. This is especially useful if you intend to link other things (like i.e. Zigbee or Shelly switches) to the TA via OH that can be controlled by different sources. This prevents unintended toggles or even toggle-loops. +### Some additional hints and comments + +You might already have noticed that some state information is in German. +As I have set the `Accept-Language`-Http-Header to `en` for all request and found no other way setting a language for the schema pages I assume it is a lack of internationalization in the C.M.I. +You could circumvent this by creating map files to map things properly to your language. + +If you want to see the possible options of a multi-state field you could open the _schema API page_ with your web browser and click on the object. +A Popup with an option field will be shown showing all possible options, like in this screenshot: + +![screenshot-operation-mode-values](doc/images/operation-mode-values.png) + +Please be also aware that there are field having more 'state' values than options, i.E. a manual output override: It has 'Auto/On', 'Auto/Off', 'Manual/On', 'Manual/Off' as state, but only 'Auto', 'Manual/On' and 'Manual/Off' as updateable option. +You only set it to 'Auto' and the extension On/Off is added depending on the system's current state. + +## CoE + +### Configure CAN outputs in TAPPS2 + +You need to configure CAN outputs in your Functional data on the UVR16x2. +This can be done by using the TAPPS2 application from TA. Follow the user guide on how to do this. + +### Configure your CMI for CoE + +Now follow the User Guide of the CMI on how to setup CAN over Ethernet (COE). +Here you will map your outputs that you configured in the previous step. +This can be accomplished via the GUI on the CMI or via the coe.csv file. +As the target device you need to put the IP of your openHAB server. +Don’t forget to reboot the CMI after you uploaded the coe.csv file. + ### TA C.M.I. CoE Connection +The _TA C.M.I. CoE Connection_ has to be manually configured. + +This thing reflects a connection to a node behind a specific C.M.I.. This node could be every CAN-Capable device from TA which allows to define a CAN-Input. + +| Parameter Label | Parameter ID | Description | Accepted values | +|-------------------------|-----------------|---------------------------------------------------------------------------------------------------------------|------------------------| +| C.M.I. IP Address | host | Host name or IP address of the C.M.I | host name or ip | +| Node | node | The CoE / CAN Node number openHAB should represent | 1-64 | + +The thing has no channels by default - they have to be added manually matching the configured inputs / outputs for the related CAN Node. Digital and Analog channels are supported. Please read TA's documentation related to the CAN-protocol - multiple analog (4) and digital (16) channels are combined so please be aware of this design limitation. + +### CoE Connection Channels + Some comments on the CoE Connection and channel configuration: As you might already have taken notice when studying the TA's manual, there are always a multiple CoE-values updated within a single CoE-message. This is a design decision made by TA. @@ -212,7 +225,7 @@ The known measure types are: | 16 | dimensionless | [none] | use for multiplexers, etc | | 17.. | repeating again from 1, e.g 17==1, 18==2, ... | -## Full Example +## Full Example (CoE/Schema API) As there is no common configuration as everything depends on the configuration of the TA devices. So we just can provide some samples providing the basics so you can build the configuration matching your system. @@ -264,18 +277,20 @@ sitemap heatingTA label="heatingTA" } ``` -## Some additional hints and comments +## JSON API -Some additional hints and comments: +Before setting up the JSON API, it is worth reading the [API documention](https://wiki.ta.co.at/C.M.I._JSON-API). +Once configured, the exposed items should show up as channels. -You might already have noticed that some state information is in German. -As I have set the `Accept-Language`-Http-Header to `en` for all request and found no other way setting a language for the schema pages I assume it is a lack of internationalization in the C.M.I. -You could circumvent this by creating map files to map things properly to your language. +### Configuring the JSON API -If you want to see the possible options of a multi-state field you could open the _schema API page_ with your web browser and click on the object. -A Popup with an option field will be shown showing all possible options, like in this screenshot: - -![screenshot-operation-mode-values](doc/images/operation-mode-values.png) +The _TA C.M.I. JSON API Connection_ has to be manually configured. -Please be also aware that there are field having more 'state' values than options, i.E. a manual output override: It has 'Auto/On', 'Auto/Off', 'Manual/On', 'Manual/Off' as state, but only 'Auto', 'Manual/On' and 'Manual/Off' as updateable option. -You only set it to 'Auto' and the extension On/Off is added depending on the system's current state. +| Parameter Label | Parameter ID | Description | Accepted values | +| ----------------- | ------------ | ------------------------------------------------------------------------------- | ----------------------------- | +| C.M.I. IP Address | host | Host name or IP address of the C.M.I. | Any String | +| User name | username | User name for authentication on the C.M.I. | Any String | +| Password | password | Password for authentication on the C.M.I. | Any String | +| Node Id | nodeId | The node ID of the device you want to monitor (CMI → CAN-Bus) | An Integer that is not 0 | +| API-Parameters | params | The params to query. E.g. I,O,Sg (See the [API documentation](https://wiki.ta.co.at/C.M.I._JSON-API) for details)
Possible options are: Inputs (I), Outputs (O), General (Sg), Logging Analog (La), Logging Digital (Ld) | Any String | +| Poll Interval | pollInterval | Poll interval in seconds. The documentation suggests 60s, but less is possible. | An integer between 10 and 900 | diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java index d886aa9c288e8..1c5fb15ee1d95 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiBindingConstants.java @@ -31,6 +31,7 @@ public class TACmiBindingConstants { public static final ThingTypeUID THING_TYPE_CMI = new ThingTypeUID(BINDING_ID, "cmi"); public static final ThingTypeUID THING_TYPE_COE_BRIDGE = new ThingTypeUID(BINDING_ID, "coe-bridge"); public static final ThingTypeUID THING_TYPE_CMI_SCHEMA = new ThingTypeUID(BINDING_ID, "cmiSchema"); + public static final ThingTypeUID THING_TYPE_CMI_JSON = new ThingTypeUID(BINDING_ID, "cmiJSON"); public static final String CONFIG_DESCRIPTION_API_SCHEMA_DEFAULTS = "channel-type:tacmi:schemaApiDefaults"; diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java index d3b27398aada1..93af255fe10af 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/TACmiHandlerFactory.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.tacmi.internal.coe.TACmiCoEBridgeHandler; import org.openhab.binding.tacmi.internal.coe.TACmiHandler; +import org.openhab.binding.tacmi.internal.json.TACmiJsonHandler; import org.openhab.binding.tacmi.internal.schema.TACmiSchemaHandler; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; @@ -47,7 +48,8 @@ public class TACmiHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( - Stream.of(THING_TYPE_CMI, THING_TYPE_COE_BRIDGE, THING_TYPE_CMI_SCHEMA).collect(Collectors.toSet())); + Stream.of(THING_TYPE_CMI, THING_TYPE_COE_BRIDGE, THING_TYPE_CMI_SCHEMA, THING_TYPE_CMI_JSON) + .collect(Collectors.toSet())); private final HttpClient httpClient; private final TACmiChannelTypeProvider channelTypeProvider; @@ -74,6 +76,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new TACmiCoEBridgeHandler((Bridge) thing); } else if (THING_TYPE_CMI_SCHEMA.equals(thingTypeUID)) { return new TACmiSchemaHandler(thing, httpClient, channelTypeProvider); + } else if (THING_TYPE_CMI_JSON.equals(thingTypeUID)) { + return new TACmiJsonHandler(thing, httpClient); } return null; diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/Config.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/Config.java new file mode 100644 index 0000000000000..5ade47a89d0fc --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/Config.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class holding the configuration of the TA Json binding. + * + * @author Moritz 'Morty' Strübe - Initial contribution + * + */ +@NonNullByDefault +public class Config { + + /** + * host address of the C.M.I. + */ + public String host = ""; + + /** + * Username + */ + public String username = ""; + + /** + * Password + */ + public String password = ""; + + /** + * Node-ID + */ + public Integer nodeId = 1; + + /** + * Json-Params + */ + public String params = "I,O,SG"; + + /** + * API page poll interval + */ + public int pollInterval = 60; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/TACmiJsonHandler.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/TACmiJsonHandler.java new file mode 100644 index 0000000000000..1dedee18aaf9e --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/TACmiJsonHandler.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.tacmi.internal.json.obj.IO; +import org.openhab.binding.tacmi.internal.json.obj.JsonResponse; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Handler for the TA Json bindung + * + * @author Moritz 'Morty' Strübe - Initial contribution + * + */ +@NonNullByDefault +public class TACmiJsonHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(TACmiJsonHandler.class); + private HttpClient httpClient; + private @Nullable String authHeader; + private @Nullable String url; + private @Nullable ScheduledFuture scheduledFuture; + private int pollInterval = 60; + + public TACmiJsonHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.httpClient = httpClient; + } + + @Override + public void initialize() { + final Config config = getConfigAs(Config.class); + + if (config.host.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No host configured!"); + return; + } + if (config.username.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No username configured!"); + return; + } + if (config.password.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No password configured!"); + return; + } + if (config.params.isBlank()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No params configured!"); + return; + } + + if (config.nodeId == 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "NodeId not configured!"); + return; + } + + if (config.pollInterval < 10) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Poll interval to short"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + this.authHeader = "Basic " + Base64.getEncoder() + .encodeToString((config.username + ":" + config.password).getBytes(StandardCharsets.ISO_8859_1)); + + this.url = "http://" + config.host + "/INCLUDE/api.cgi?" + "jsonnode=" + config.nodeId + "&jsonparam=" + + config.params; + + logger.debug("URL: {}", this.url); + + this.pollInterval = config.pollInterval; + + // we want to trigger the initial refresh 'at once' + this.scheduledFuture = scheduler.scheduleWithFixedDelay(this::refreshData, 0, pollInterval, TimeUnit.SECONDS); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // We do not support commands + } + + @Override + public void dispose() { + final ScheduledFuture scheduledFuture = this.scheduledFuture; + if (scheduledFuture != null) { + scheduledFuture.cancel(true); + this.scheduledFuture = null; + } + super.dispose(); + } + + private void refreshData() { + try { + final Request req = httpClient.newRequest(this.url).method(HttpMethod.GET).timeout(10000, + TimeUnit.MILLISECONDS); + req.header(HttpHeader.ACCEPT_LANGUAGE, "en"); + final String authH = this.authHeader; + if (authH != null) { + req.header(HttpHeader.AUTHORIZATION, authH); + } + + final ContentResponse httpResponse = req.send(); + if (httpResponse.getStatus() != 200) { + logger.warn("Error requesting update {} / {} \n{}", httpResponse.getStatus(), httpResponse.getReason(), + httpResponse.getContentAsString()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } + logger.trace("Reply:\n{}", httpResponse.getContentAsString()); + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); + JsonResponse jsonResponse = gson.fromJson(httpResponse.getContentAsString(), JsonResponse.class); + if (jsonResponse == null) { + logger.warn("Response is Empty"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + return; + } + if (jsonResponse.statusCode != 0) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, jsonResponse.status); + return; + } + updateChannels(jsonResponse); + updateStatus(ThingStatus.ONLINE); + } catch (final InterruptedException e) { + // binding shutdown is in progress + updateStatus(ThingStatus.OFFLINE); + } catch (final Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void updateChannels(JsonResponse jsonResponse) throws Exception { + final var allChans = new ArrayList(); + + BiFunction, String, Boolean> chanhandler = (jsonArray, prefix) -> { + var channelChanged = false; + for (var inputItem : jsonArray) { + var name = prefix.charAt(0) + inputItem.number.toString(); + final var type = inputItem.getType(); + final var channelType = inputItem.getChannelType(); + Channel channel = this.getThing().getChannel(name); + final var currentChannelType = channel != null ? channel.getChannelTypeUID() : null; + // The null checker does not detect that if channel is null, currentChannelType is also null. + if (channel == null || currentChannelType == null || !currentChannelType.equals(channelType)) { + logger.debug("Creating / updating channel {} of type {} for '{}'", name, type, inputItem.getDesc()); + ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), name); + ChannelBuilder channelBuilder = ChannelBuilder.create(channelUID, type); + channelBuilder.withLabel(prefix + " " + inputItem.number.toString()); + channelBuilder.withDescription(inputItem.getDesc()); + channelBuilder.withType(channelType); + channel = channelBuilder.build(); + channelChanged = true; + } + allChans.add(channel); + updateState(channel.getUID(), inputItem.getState()); + } + return channelChanged; + }; + + boolean modified = false; + modified |= chanhandler.apply(jsonResponse.data.inputs, "Input"); + modified |= chanhandler.apply(jsonResponse.data.outputs, "Output"); + modified |= chanhandler.apply(jsonResponse.data.general, "Global"); + + if (modified) { + final var eThing = editThing(); + eThing.withChannels(allChans); + updateThing(eThing.build()); + } + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Data.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Data.java new file mode 100644 index 0000000000000..e9fe50bbf15b8 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Data.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj; + +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class holding the Data JSON element + * + * @author Moritz 'Morty' Strübe - Initial contribution + */ +@NonNullByDefault +public class Data { + public Collection inputs = Collections.emptyList(); + public Collection outputs = Collections.emptyList(); + public Collection general = Collections.emptyList(); +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Header.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Header.java new file mode 100644 index 0000000000000..d05bbe486b25b --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Header.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class holding the Header JSON element + * + * @author Moritz 'Morty' Strübe - Initial contribution + */ +@NonNullByDefault +public class Header { + public Integer version = -1; + public String device = ""; + public Integer timestamp = 0; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/IO.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/IO.java new file mode 100644 index 0000000000000..f72c554c75588 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/IO.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.State; + +/** + * Class holding the IO JSON element + * + * @author Moritz 'Morty' Strübe - Initial contribution + */ +@NonNullByDefault +public class IO { + public Integer number = 0; + public String ad = ""; + public Value value = new Value(); + + public @Nullable String getType() { + return value.getType(); + } + + public State getState() { + return value.getState(); + } + + public String getDesc() { + return value.getDesc(); + } + + public @Nullable ChannelTypeUID getChannelType() { + return value.getChannelType(); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/JsonResponse.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/JsonResponse.java new file mode 100644 index 0000000000000..6b60004f556ec --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/JsonResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * Class holding the Data JSON element + * + * @author Moritz 'Morty' Strübe - Initial contribution + */ +@NonNullByDefault +public class JsonResponse { + public Header header = new Header(); + public Data data = new Data(); + public String status = "Not Set (JSON)"; + @SerializedName("Status code") + public Integer statusCode = -7337; +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Value.java b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Value.java new file mode 100644 index 0000000000000..54e63501fb6e4 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/main/java/org/openhab/binding/tacmi/internal/json/obj/Value.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.measure.Unit; +import javax.measure.quantity.ElectricCurrent; +import javax.measure.quantity.Length; +import javax.measure.quantity.Mass; +import javax.measure.quantity.Power; +import javax.measure.quantity.Speed; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.tacmi.internal.TACmiBindingConstants; +import org.openhab.core.library.dimension.Density; +import org.openhab.core.library.dimension.VolumetricFlowRate; +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.unit.MetricPrefix; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.State; + +import tech.units.indriya.format.SimpleUnitFormat; +import tech.units.indriya.function.MultiplyConverter; +import tech.units.indriya.unit.ProductUnit; +import tech.units.indriya.unit.TransformedUnit; + +/** + * Class holding the Value JSON element + * + * @author Moritz 'Morty' Strübe - Initial contribution + */ +@NonNullByDefault +public class Value { + public double value = 0; + public String unit = "-1"; + private @Nullable TypeInfo typeInfo = null; + + // Always return a "valid" type info + // This simplifies the rest of the code, as less error checking is needed. + private TypeInfo getTypeInfo() { + // Need a local variable to satisfy null-checks + var rv = typeInfo; + if (rv != null) { + return rv; + } + + try { + rv = TYPE_MAP.get(Integer.parseInt(unit)); + } catch (Exception e) { + } + if (rv == null) { + rv = new TypeInfo("Undefined: " + unit, "Number", null); + } + typeInfo = rv; + return rv; + } + + public String getDesc() { + return getTypeInfo().desc; + } + + public @Nullable String getType() { + return getTypeInfo().unitname; + } + + public ChannelTypeUID getChannelType() { + final var ti = getTypeInfo(); + if (!ti.unitname.startsWith("Number")) { + return TACmiBindingConstants.CHANNEL_TYPE_SCHEME_STATE_RO_UID; + } + return TACmiBindingConstants.CHANNEL_TYPE_SCHEME_NUMERIC_RO_UID; + } + + public State getState() { + final var ti = getTypeInfo(); + if (ti.unitname.startsWith("Switch")) { + return (value != 0) ? OnOffType.ON : OnOffType.OFF; + } + final var unit = ti.unit; + if (unit != null) { + return new QuantityType<>(value, unit); + } + return new DecimalType(Double.valueOf(value)); + } + + static class TypeInfo { + public final String desc; + public final String unitname; + public final @Nullable Unit<@NonNull ?> unit; + + public TypeInfo(String desc, String unitname, @Nullable Unit<@NonNull ?> unit) { + this.desc = desc; + this.unitname = unitname; + this.unit = unit; + } + } + + static final Unit LITRE_PER_HOUR = new ProductUnit<>( + tech.units.indriya.unit.Units.LITRE.divide(tech.units.indriya.unit.Units.HOUR)); + static final Unit LITRE_PER_DAY = new ProductUnit<>( + tech.units.indriya.unit.Units.LITRE.divide(tech.units.indriya.unit.Units.DAY)); + static final Unit KILOWATT = MetricPrefix.KILO(Units.WATT); + static final Unit MILLIAMPERE = MetricPrefix.MILLI(Units.AMPERE); + static final Unit KILOOHM = MetricPrefix.KILO(Units.WATT); + static final Unit KILOMETRE = MetricPrefix.KILO(SIUnits.METRE); + static final Unit CENTIMETRE = MetricPrefix.CENTI(SIUnits.METRE); + static final Unit MILLIMETRE = MetricPrefix.MILLI(SIUnits.METRE); + static final Unit MILLIMETRE_PER_MINUTE = new TransformedUnit<>("mm/min", Units.MILLIMETRE_PER_HOUR, + MultiplyConverter.ofRational(BigInteger.ONE, BigInteger.valueOf(60))); + static final Unit MILLIMETRE_PER_DAY = new TransformedUnit<>("mm/d", Units.MILLIMETRE_PER_HOUR, + MultiplyConverter.ofRational(BigInteger.valueOf(24), BigInteger.ONE)); + static final Unit GRAM_PER_CUBICMETRE = new TransformedUnit<>("g/m³", Units.KILOGRAM_PER_CUBICMETRE, + MultiplyConverter.ofRational(BigInteger.ONE, BigInteger.valueOf(1000))); + static final Unit TONNE = new TransformedUnit<>("t", SIUnits.KILOGRAM, + MultiplyConverter.ofRational(BigInteger.valueOf(1000), BigInteger.ONE)); + static final Unit GRAMM = new TransformedUnit<>("g", SIUnits.KILOGRAM, + MultiplyConverter.ofRational(BigInteger.ONE, BigInteger.valueOf(1000))); + + static { + SimpleUnitFormat.getInstance().label(LITRE_PER_HOUR, "l/h"); + SimpleUnitFormat.getInstance().label(LITRE_PER_DAY, "l/d"); + SimpleUnitFormat.getInstance().label(KILOWATT, "kW"); + SimpleUnitFormat.getInstance().label(MILLIAMPERE, "mA"); + SimpleUnitFormat.getInstance().label(KILOOHM, "kΩ"); + SimpleUnitFormat.getInstance().label(KILOMETRE, "km"); + SimpleUnitFormat.getInstance().label(CENTIMETRE, "cm"); + SimpleUnitFormat.getInstance().label(MILLIMETRE, "mm"); + SimpleUnitFormat.getInstance().label(MILLIMETRE_PER_MINUTE, "mm/min"); + SimpleUnitFormat.getInstance().label(MILLIMETRE_PER_DAY, "mm/d"); + SimpleUnitFormat.getInstance().label(GRAM_PER_CUBICMETRE, "g/m³"); + SimpleUnitFormat.getInstance().label(TONNE, "t"); + SimpleUnitFormat.getInstance().label(GRAMM, "g"); + } + + static final Map TYPE_MAP; + static { + // @formatter:off + // -1 is reserved to return null + Map lMap = new HashMap(); + lMap.put(0, new TypeInfo("Unknown", "Number", null)); + lMap.put(1, new TypeInfo("°C", "Number:Temperature", SIUnits.CELSIUS)); + lMap.put(2, new TypeInfo("W/m²", "Number:Intensity", Units.WATT_HOUR_PER_SQUARE_METRE)); + lMap.put(3, new TypeInfo("l/h", "Number:VolumetricFlowRate", LITRE_PER_HOUR)); + lMap.put(4, new TypeInfo("sec", "Number:Time", Units.SECOND)); + lMap.put(5, new TypeInfo("min", "Number:Time", Units.MINUTE)); + lMap.put(6, new TypeInfo("l/Imp", "Number", null)); + lMap.put(7, new TypeInfo("K", "Number:Temperature", Units.KELVIN)); + lMap.put(8, new TypeInfo("%", "Number:Dimensionless", Units.PERCENT)); + lMap.put(10, new TypeInfo("kW", "Number:Power", KILOWATT)); + lMap.put(11, new TypeInfo("kWh", "Number:Energy", Units.KILOWATT_HOUR)); + lMap.put(12, new TypeInfo("MWh", "Number:Energy", Units.MEGAWATT_HOUR)); + lMap.put(13, new TypeInfo("V", "Number:ElectricPotential", Units.VOLT)); + lMap.put(14, new TypeInfo("mA", "Number:ElectricCurrent", MILLIAMPERE)); + lMap.put(15, new TypeInfo("hr", "Number:Duration", Units.HOUR)); + lMap.put(16, new TypeInfo("Days", "Number:Duration", Units.DAY)); + lMap.put(17, new TypeInfo("Imp", "Number", null)); + lMap.put(18, new TypeInfo("kΩ", "Number:ElectricResistance", KILOOHM)); + lMap.put(19, new TypeInfo("l", "Number:Volume", Units.LITRE)); + lMap.put(20, new TypeInfo("km/h", "Number:Speed", SIUnits.KILOMETRE_PER_HOUR)); + lMap.put(21, new TypeInfo("Hz", "Number:Frequency", Units.HERTZ)); + lMap.put(22, new TypeInfo("l/min", "Number:VolumetricFlowRate", Units.LITRE_PER_MINUTE)); + lMap.put(23, new TypeInfo("bar", "Number:Pressure", Units.BAR)); + lMap.put(24, new TypeInfo("Unknown", "Number", null)); + lMap.put(25, new TypeInfo("km", "Number:Length", KILOMETRE)); + lMap.put(26, new TypeInfo("m", "Number:Length", SIUnits.METRE)); + lMap.put(27, new TypeInfo("mm", "Number:Length", MILLIMETRE)); + lMap.put(28, new TypeInfo("m³", "Number:Volume", SIUnits.CUBIC_METRE)); + lMap.put(35, new TypeInfo("l/d", "Number:VolumetricFlowRate", LITRE_PER_DAY)); + lMap.put(36, new TypeInfo("m/s", "Number:Speed", Units.METRE_PER_SECOND)); + lMap.put(37, new TypeInfo("m³/min", "Number:VolumetricFlowRate", Units.CUBICMETRE_PER_MINUTE)); + lMap.put(38, new TypeInfo("m³/h", "Number:VolumetricFlowRate", Units.CUBICMETRE_PER_HOUR)); + lMap.put(39, new TypeInfo("m³/d", "Number:VolumetricFlowRate", Units.CUBICMETRE_PER_DAY)); + lMap.put(40, new TypeInfo("mm/min", "Number:Speed", MILLIMETRE_PER_MINUTE)); + lMap.put(41, new TypeInfo("mm/h", "Number:Speed", Units.MILLIMETRE_PER_HOUR)); + lMap.put(42, new TypeInfo("mm/d", "Number:Speed", MILLIMETRE_PER_DAY)); + lMap.put(43, new TypeInfo("ON/OFF", "Switch", null)); + lMap.put(44, new TypeInfo("NO/YES", "Switch", null)); + lMap.put(46, new TypeInfo("°C", "Number:Temperature", SIUnits.CELSIUS)); + lMap.put(50, new TypeInfo("€", "Number", null)); + lMap.put(51, new TypeInfo("$", "Number", null)); + lMap.put(52, new TypeInfo("g/m³", "Number:Density", GRAM_PER_CUBICMETRE)); + lMap.put(53, new TypeInfo("Unknown", "Number", null)); + lMap.put(54, new TypeInfo("°", "Number:Angle", Units.DEGREE_ANGLE)); + lMap.put(56, new TypeInfo("°", "Number:Angle", Units.DEGREE_ANGLE)); + lMap.put(57, new TypeInfo("sec", "Number:Duration", Units.SECOND)); + lMap.put(58, new TypeInfo("Unknown", "Number", null)); + lMap.put(59, new TypeInfo("%", "Number:Dimensionless", Units.PERCENT)); + lMap.put(60, new TypeInfo("h", "Number:Time", Units.HOUR)); + lMap.put(63, new TypeInfo("A", "Number:Current", Units.AMPERE)); + lMap.put(65, new TypeInfo("mbar", "Number:Pressure", Units.MILLIBAR)); + lMap.put(66, new TypeInfo("Pa", "Number:Pressure", SIUnits.PASCAL)); + lMap.put(67, new TypeInfo("ppm", "Number:Dimensionless", Units.PARTS_PER_MILLION)); + lMap.put(68, new TypeInfo("Unknown", "Number", null)); + lMap.put(69, new TypeInfo("W", "Number:Power", Units.WATT)); + lMap.put(70, new TypeInfo("t", "Number:Mass", TONNE)); + lMap.put(71, new TypeInfo("kg", "Number:Mass", SIUnits.KILOGRAM)); + lMap.put(72, new TypeInfo("g", "Number:Mass", GRAMM)); + lMap.put(73, new TypeInfo("cm", "Number:Length", CENTIMETRE)); + lMap.put(74, new TypeInfo("K", "Number:Temperature", Units.KELVIN)); + lMap.put(75, new TypeInfo("lx", "Number:Illuminance", Units.LUX)); + // I assume this should be Bq/m³, but I'll stick to the JSon documentation + lMap.put(76, new TypeInfo("Bg/m³", "Number:Illuminance", Units.BECQUEREL_PER_CUBIC_METRE)); + // @formatter:on + TYPE_MAP = Collections.unmodifiableMap(lMap); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties index 9ac3589e482aa..4c6ea180efbeb 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/i18n/tacmi.properties @@ -7,6 +7,8 @@ addon.tacmi.description = This is the binding for TA C.M.I. thing-type.tacmi.cmi.label = TA C.M.I. CoE Connection thing-type.tacmi.cmi.description = CoE Communication to the "Technische Alternative C.M.I. Control and Monitoring Interface" +thing-type.tacmi.cmiJSON.label = TA C.M.I. JSON API Connection +thing-type.tacmi.cmiJSON.description = Communication via the "JSONAPI" on a "Technische Alternative C.M.I. Control and Monitoring Interface" thing-type.tacmi.cmiSchema.label = TA C.M.I. Schema API Connection thing-type.tacmi.cmiSchema.description = Communication to a special "API" schema page on a "Technische Alternative C.M.I. Control and Monitoring Interface" thing-type.tacmi.coe-bridge.label = TA C.M.I. CoE Bridge @@ -18,6 +20,18 @@ thing-type.config.tacmi.cmi.host.label = C.M.I. IP Address thing-type.config.tacmi.cmi.host.description = Host name of IP address of the CMI thing-type.config.tacmi.cmi.node.label = Node thing-type.config.tacmi.cmi.node.description = The CoE / CAN Node number openHAB should represent +thing-type.config.tacmi.cmiJSON.host.label = C.M.I. IP Address +thing-type.config.tacmi.cmiJSON.host.description = Host name or IP address of the C.M.I. +thing-type.config.tacmi.cmiJSON.nodeId.label = Node Id +thing-type.config.tacmi.cmiJSON.nodeId.description = The node ID of the device you want to monitor (CMI → CAN-Bus) +thing-type.config.tacmi.cmiJSON.params.label = API-Parameters +thing-type.config.tacmi.cmiJSON.params.description = The params to query. E.g. I,O,Sg (See the API documentation for details)
Possible options are: Inputs (I), Outputs (O), General (Sg), Logging Analog (La), Logging Digital (Ld) +thing-type.config.tacmi.cmiJSON.password.label = Password +thing-type.config.tacmi.cmiJSON.password.description = Password for authentication on the C.M.I. +thing-type.config.tacmi.cmiJSON.pollInterval.label = Poll Interval +thing-type.config.tacmi.cmiJSON.pollInterval.description = Poll interval in seconds. The documentation suggests 60s, but less is possible. +thing-type.config.tacmi.cmiJSON.username.label = User Name +thing-type.config.tacmi.cmiJSON.username.description = User name for authentication on the C.M.I. thing-type.config.tacmi.cmiSchema.host.label = C.M.I. IP Address thing-type.config.tacmi.cmiSchema.host.description = Host name or IP address of the C.M.I. thing-type.config.tacmi.cmiSchema.password.label = Password diff --git a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml index 6a4277cbb9e73..b3bccbfce0e8a 100644 --- a/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.tacmi/src/main/resources/OH-INF/thing/thing-types.xml @@ -151,27 +151,23 @@ An On/Off state read from C.M.I. - Switch A modifiable On/Off state read from C.M.I. - Number A numeric value read from C.M.I. - - + String A state value read from C.M.I. - DateTime @@ -180,4 +176,47 @@ + + + + + + Communication via the "JSONAPI" on a "Technische Alternative C.M.I. Control and Monitoring + Interface" + + + + + Host name or IP address of the C.M.I. + network-address + + + + User name for authentication on the C.M.I. + + + + Password for authentication on the C.M.I. + password + + + + The node ID of the device you want to monitor (CMI → CAN-Bus) + + + + API documentation for details)
+ Possible options are: Inputs (I), Outputs (O), General (Sg), Logging Analog (La), Logging Digital (Ld) ]]> +
+ I,O,Sg +
+ + + Poll interval in seconds. The documentation suggests 60s, but less is possible. + 60 + true + +
+
+ diff --git a/bundles/org.openhab.binding.tacmi/src/test/java/org/openhab/binding/tacmi/internal/json/obj/test/DeSer.java b/bundles/org.openhab.binding.tacmi/src/test/java/org/openhab/binding/tacmi/internal/json/obj/test/DeSer.java new file mode 100644 index 0000000000000..4787316c27f07 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/test/java/org/openhab/binding/tacmi/internal/json/obj/test/DeSer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2025 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.tacmi.internal.json.obj.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.FileReader; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.tacmi.internal.json.obj.IO; +import org.openhab.binding.tacmi.internal.json.obj.JsonResponse; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonReader; + +/** + * Test the GSON object mapper + * + * @author Moritz 'Morty' Strübe - Initial contribution + * + */ +@NonNullByDefault +class DeSer { + @Test + void test() { + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create(); + JsonReader reader; + reader = assertDoesNotThrow(() -> { + return new JsonReader(new FileReader("src/test/resources/ex.json")); + }); + + JsonResponse resp = gson.fromJson(reader, JsonResponse.class); + + assertEquals(0, resp.statusCode); + assertEquals(5, resp.header.version); + var inp = resp.data.inputs.toArray(new IO[0]); + assertEquals(50.2, inp[0].value.value); + } +} diff --git a/bundles/org.openhab.binding.tacmi/src/test/resources/ex.json b/bundles/org.openhab.binding.tacmi/src/test/resources/ex.json new file mode 100644 index 0000000000000..8516b1edb40b9 --- /dev/null +++ b/bundles/org.openhab.binding.tacmi/src/test/resources/ex.json @@ -0,0 +1,324 @@ +{ + "Header": { + "Version": 5, + "Device": "87", + "Timestamp": 1664795104 + }, + "Data": { + "Inputs": [ + { + "Number": 1, + "AD": "A", + "Value": { + "Value": 50.2, + "Unit": "1" + } + }, + { + "Number": 2, + "AD": "A", + "Value": { + "Value": 45.6, + "Unit": "1" + } + }, + { + "Number": 3, + "AD": "A", + "Value": { + "Value": 39.4, + "Unit": "1" + } + }, + { + "Number": 4, + "AD": "A", + "Value": { + "Value": 9999.9, + "Unit": "1" + } + }, + { + "Number": 5, + "AD": "A", + "Value": { + "Value": 12.2, + "Unit": "1" + } + }, + { + "Number": 6, + "AD": "A", + "Value": { + "Value": 29.3, + "Unit": "1" + } + }, + { + "Number": 7, + "AD": "A", + "Value": { + "Value": 39.2, + "Unit": "1" + } + }, + { + "Number": 8, + "AD": "A", + "Value": { + "Value": 48.8, + "Unit": "1" + } + }, + { + "Number": 9, + "AD": "A", + "Value": { + "Value": 40.4, + "Unit": "1" + } + }, + { + "Number": 10, + "AD": "A", + "Value": { + "Value": 9999.9, + "Unit": "1" + } + }, + { + "Number": 11, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 12, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 13, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 14, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 15, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 16, + "AD": "D", + "Value": { + "Value": 1, + "Unit": "43" + } + } + ], + "Outputs": [ + { + "Number": 5, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 8, + "AD": "D", + "Value": { + "Value": 1, + "Unit": "43" + } + }, + { + "Number": 9, + "AD": "D", + "Value": { + "Value": 1, + "Unit": "43" + } + }, + { + "Number": 10, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 11, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 12, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 13, + "AD": "A", + "Value": { + "State": 0, + "Value": 0.00, + "Unit": "13" + } + }, + { + "Number": 14, + "AD": "A", + "Value": { + "State": 0, + "Value": 0.00, + "Unit": "13" + } + }, + { + "Number": 15, + "AD": "A", + "Value": { + "State": 0, + "Value": 0.00, + "Unit": "13" + } + } + ], + "General": [ + { + "Number": 1, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 2, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "44" + } + }, + { + "Number": 3, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "44" + } + }, + { + "Number": 4, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "44" + } + }, + { + "Number": 5, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "44" + } + }, + { + "Number": 6, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "44" + } + }, + { + "Number": 7, + "AD": "A", + "Value": { + "Value": 50.90, + "Unit": "21" + } + }, + { + "Number": 8, + "AD": "A", + "Value": { + "Value": 1, + "Unit": "0" + } + }, + { + "Number": 9, + "AD": "D", + "Value": { + "Value": 1, + "Unit": "44" + } + }, + { + "Number": 10, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 11, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 12, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + }, + { + "Number": 13, + "AD": "D", + "Value": { + "Value": 0, + "Unit": "43" + } + } + ] + }, + "Status": "OK", + "Status code": 0 +} \ No newline at end of file