diff --git a/CODEOWNERS b/CODEOWNERS
index ae0d5d1a3c958..f2eb395a8a376 100755
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -164,7 +164,7 @@
/bundles/org.openhab.binding.icalendar/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.icloud/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.ihc/ @paulianttila
-/bundles/org.openhab.binding.insteon/ @openhab/add-ons-maintainers
+/bundles/org.openhab.binding.insteon/ @jsetton
/bundles/org.openhab.binding.intesis/ @hmerk
/bundles/org.openhab.binding.iotawatt/ @PRosenb
/bundles/org.openhab.binding.ipcamera/ @Skinah
diff --git a/bundles/org.openhab.binding.insteon/README.md b/bundles/org.openhab.binding.insteon/README.md
index e033abe0beb66..4bee1a400ea92 100644
--- a/bundles/org.openhab.binding.insteon/README.md
+++ b/bundles/org.openhab.binding.insteon/README.md
@@ -1,266 +1,555 @@
# Insteon Binding
-Insteon is a home area networking technology developed primarily for connecting light switches and loads.
-Insteon devices send messages either via the power line, or by means of radio frequency (RF) waves, or both (dual-band.
-A considerable number of Insteon compatible devices such as switchable relays, thermostats, sensors etc are available.
+Insteon is a proprietary home automation system that enables light switches, lights, thermostats, leak sensors, remote controls, motion sensors, and other electrically powered devices to interoperate through power lines, radio frequency (RF) communications, or both (dual-band)
More about Insteon can be found on [Wikipedia](https://en.wikipedia.org/wiki/Insteon).
-This binding provides access to the Insteon network by means of either an Insteon PowerLinc Modem (PLM), a legacy Insteon Hub 2242-222 or the current 2245-222 Insteon Hub.
-The modem can be connected to the openHAB server either via a serial port (Model 2413S) or a USB port (Model 2413U.
+It provides access to the Insteon network by means of either an Insteon PowerLinc Modem (PLM), the legacy 2242-222 Insteon Hub or the current 2245-222 Insteon Hub 2.
+The modem can be connected to the openHAB server either via a serial port (Model 2413S) or a USB port (Model 2413U).
The Insteon PowerLinc Controller (Model 2414U) is not supported since it is a PLC not a PLM.
-The modem can also be connected via TCP (such as ser2net.
+The modem can also be connected via TCP (such as ser2net).
The binding translates openHAB commands into Insteon messages and sends them on the Insteon network.
-Relevant messages from the Insteon network (like notifications about switches being toggled) are picked up by the modem and converted to openHAB status updates by the binding.
+Relevant messages from the Insteon network (like notifications about switches being toggled) are picked up by the modem and converted to openHAB state updates by the binding.
The binding also supports sending and receiving of legacy X10 messages.
-The binding does not support linking new devices on the fly, i.e. all devices must be linked with the modem _before_ starting the Insteon binding.
+The openHAB binding supports configuring most of the device local settings, linking a device to the modem, managing link database records and scenes along with monitoring inbound/outbound messages.
+Other tools can be used to managed Insteon devices, such as the [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) open source project, or the [HouseLinc](https://www.insteon.com/houselinc) software from Insteon can also be used for configuration, but it wipes the modem link database clean on its initial use, requiring to re-link the modem to all devices.
-The openHAB binding supports minimal configuration of devices, currently only monitoring and sending messages.
-For all other configuration and set up of devices, link the devices manually via the set buttons, or use the free [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) software.
-The free HouseLinc software from Insteon can also be used for configuration, but it wipes the modem link database clean on its initial use, requiring to re-link the modem to all devices.
+At startup, the binding will download the modem database along with each configured device all-link database if not previously downloaded and currently awake.
+Therefore, the initialization on the first start may take some additional time to complete depending on the number of devices configured.
+The modem and device link databases are only downloaded once unless the binding receives an indication that a database was updated or marked to be refreshed via the [openHAB console](#console-commands).
+
+**Important note as of openHAB 4.3.0**
+
+The binding has been rewritten to simplify the user experience by retrieving all the configuration directly from the device when possible, and improving the way the Insteon things are configured in MainUI.
+If switching from a previous release, you will need to reconfigure your Insteon environment with the new bridges, things and channels to take advantage of these enhancements.
+You can follow the [migration guide](#migration-guide).
+
+However, the new version is fully backward compatible by supporting the legacy things.
+On the first start, existing `device` things connected to a `network` bridge will be migrated to the `legacy-device` thing type while still keeping the same ids to prevent any breakage.
+It is important to note that once the migration has occurred, downgrading to an older version will not be possible.
## Supported Things
-| Thing | Type | Description |
-|----------|--------|------------------------------|
-| network | Bridge | An insteon PLM or hub that is used to communicate with the Insteon devices |
-|device| Thing | Insteon devices such as dimmers, keypads, sensors, etc. |
+| Thing | Type | Description |
+| ------ | ------ | ---------------------------------------------------------------- |
+| hub1 | Bridge | An Insteon Hub Legacy that communicates with Insteon devices. |
+| hub2 | Bridge | An Insteon Hub 2 that communicates with Insteon devices. |
+| plm | Bridge | An Insteon PLM that communicates with Insteon devices. |
+| device | Thing | An Insteon device such as a switch, dimmer, keypad, sensor, etc. |
+| scene | Thing | An Insteon scene that controls multiple devices simultaneously. |
+| x10 | Thing | An X10 device such as a switch, dimmer or sensor. |
+
+### Legacy Things
+
+| Thing | Type | Description |
+| ------------- | ------ | ----------------------------------------------------------------------- |
+| network | Bridge | An Insteon PLM or Hub that communicates with Insteon devices. |
+| legacy-device | Thing | An Insteon or X10 device such as a switch, dimmer, keypad, sensor, etc. |
## Discovery
-The network bridge is not automatically discovered, you will have to manually add the it yourself.
-Upon proper configuration of the network bridge, the network device database will be downloaded.
-Any Insteon device that exists in the database and is not currently configured is added to the inbox.
-The naming convention is **Insteon Device AABBCC**, where AA, BB and CC are from the Insteon device address.
+An Insteon bridge is not automatically discovered and will have to be manually added.
+Once configured, depending on the bridge discovery parameters, any Insteon devices or scenes that exists in the modem database and is not currently configured will be automatically be added to the inbox.
+For the legacy bridge configuration, only missing device are discovered.
+The naming convention for devices is **_Vendor_ _Model_ _Description_** if its product data is retrievable, otherwise **Insteon Device AA.BB.CC**, where `AA.BB.CC` is the Insteon device address.
+For scenes, it is **Insteon Scene 42**, where `42` is the scene group number.
+The device auto-discovery is enabled by default while disabled for scenes.
X10 devices are not auto discovered.
## Thing Configuration
-### Network Configuration
-
-The Insteon PLM or hub is configured with the following parameters:
-
-| Parameter | Default | Required | Description |
-|----------|---------:|--------:|-------------|
-| port | | Yes | **Examples:**
- PLM on Linux: `/dev/ttyS0` or `/dev/ttyUSB0`
- Smartenit ZBPLM on Linux: `/dev/ttyUSB0,baudRate=115200`
- PLM on Windows: `COM1`
- Current hub (2245-222) at 192.168.1.100 on port 25105, with a poll interval of 1000 ms (1 second): `/hub2/my_user_name:my_password@192.168.1.100:25105,poll_time=1000`
- Legacy hub (2242-222) at 192.168.1.100 on port 9761:`/hub/192.168.1.100:9761`
- Networked PLM using ser2net at 192.168.1.100 on port 9761:`/tcp/192.168.1.100:9761` |
-| devicePollIntervalSeconds | 300 | No | Poll interval of devices in seconds. Poll too often and you will overload the insteon network, leading to sluggish or no response when trying to send messages to devices. The default poll interval of 300 seconds has been tested and found to be a good compromise in a configuration of about 110 switches/dimmers. |
-| additionalDevices | | No | File with additional device types. The syntax of the file is identical to the `device_types.xml` file in the source tree. Please remember to post successfully added device types to the openhab group so the developers can include them into the `device_types.xml` file! |
-| additionalFeatures | | No | File with additional feature templates, like in the `device_features.xml` file in the source tree. |
+For bridge things, if the poll interval is too short, it will result in sluggish performance and no response when trying to send messages to devices.
+The default poll interval of 300 seconds has been tested and found to be a good compromise in a configuration of about 110 switches/dimmers.
+
+### `hub1`
+
+| Parameter | Default | Required | Description |
+| --------------------------- | :-----: | :------: | ---------------------------------------------------------------------- |
+| hostname | | Yes | Network address of the hub. |
+| port | 9761 | No | Network port of the hub. |
+| devicePollIntervalInSeconds | 300 | No | Device poll interval in seconds. |
+| deviceDiscoveryEnabled | true | No | Discover Insteon devices found in the hub database but not configured. |
+| sceneDiscoveryEnabled | false | No | Discover Insteon scenes found in the hub database but not configured. |
+| deviceSyncEnabled | false | No | Synchronize related devices based on their all-link database. |
+
+>NOTE: Use this bridge to connect to a networked PLM via ser2net.
+
+### `hub2`
+
+| Parameter | Default | Required | Description |
+| ----------------------------- | :-----: | :------: | ---------------------------------------------------------------------- |
+| hostname | | Yes | Network address of the hub. |
+| port | 25105 | No | Network port of the hub. |
+| username | | Yes | Username to access the hub. |
+| password | | Yes | Password to access the hub. |
+| hubPollIntervalInMilliseconds | 1000 | No | Hub poll interval in milliseconds. |
+| devicePollIntervalInSeconds | 300 | No | Device poll interval in seconds. |
+| deviceDiscoveryEnabled | true | No | Discover Insteon devices found in the hub database but not configured. |
+| sceneDiscoveryEnabled | false | No | Discover Insteon scenes found in the hub database but not configured. |
+| deviceSyncEnabled | false | No | Synchronize related devices based on their all-link database. |
+
+### `plm`
+
+| Parameter | Default | Required | Description |
+| --------------------------- | :-----: | :------: | ------------------------------------------------------------------------ |
+| serialPort | | Yes | Serial port connected to the modem. Example: `/dev/ttyS0` or `COM1` |
+| baudRate | 19200 | No | Serial port baud rate connected to the modem. |
+| devicePollIntervalInSeconds | 300 | No | Device poll interval in seconds. |
+| deviceDiscoveryEnabled | true | No | Discover Insteon devices found in the modem database but not configured. |
+| sceneDiscoveryEnabled | false | No | Discover Insteon scenes found in the modem database but not configured. |
+| deviceSyncEnabled | false | No | Synchronize related devices based on their all-link database. |
+
+### `device`
+
+| Parameter | Required | Description |
+| --------- | :------: | ---------------------------------------------------------------------------------- |
+| address | Yes | Insteon address of the device. It can be found on the device. Example: `12.34.56`. |
+
+The device type is automatically determined by the binding using the device product data.
+For a [battery powered device](#battery-powered-devices) that was never configured previously, it may take until the next time that device sends a broadcast message to be modeled properly.
+To speed up the process for this case, it is recommended to force the device to become awake after the associated bridge is online.
+Likewise, for a device that wasn't accessible during the binding initialization phase, press on its SET button once powered on to notify the binding that it is available.
+
+### `scene`
+
+| Parameter | Required | Description |
+| --------- | :------: | -------------------------------------------------------------------------------------------------------------------------- |
+| group | Yes | Insteon scene group number between 2 and 254. It can be found in the scene detailed information in the Insteon mobile app. |
+
+### `x10`
+
+| Parameter | Required | Description |
+| ---------- | :------: | ------------------------------------------ |
+| houseCode | Yes | X10 house code of the device. Example: `A` |
+| unitCode | Yes | X10 unit code of the device. Example: `1` |
+| deviceType | Yes | X10 device type |
+
+
+ Supported X10 device types
+
+ | Device Type | Description |
+ | ----------- | ----------- |
+ | X10_Switch | X10 Switch |
+ | X10_Dimmer | X10 Dimmer |
+ | X10_Sensor | X10 Sensor |
+
+
+### `network`
+
+| Parameter | Default | Required | Description |
+| ------------------------- | :-----: | :------: | --------------------------------------- |
+| port | | Yes | Port configuration. |
+| devicePollIntervalSeconds | 300 | No | Poll interval of devices in seconds. |
+| additionalDevices | | No | File with additional device types. |
+| additionalFeatures | | No | File with additional feature templates. |
>NOTE: For users upgrading from InsteonPLM, The parameter port_1 is now port.
-### Device Configuration
-
-The Insteon device is configured with the following required parameters:
-
-| Parameter | Description |
-|----------|-------------|
-|address|Insteon or X10 address of the device. Insteon device addresses are in the format 'xx.xx.xx', and can be found on the device. X10 device address are in the format 'x.y' and are typically configured on the device.|
-|productKey|Insteon binding product key that is used to identy the device. Every Insteon device type is uniquely identified by its Insteon product key, typically a six digit hex number. For some of the older device types (in particular the SwitchLinc switches and dimmers), Insteon does not give a product key, so an arbitrary fake one of the format Fxx.xx.xx (or Xxx.xx.xx for X10 devices) is assigned by the binding.|
-|deviceConfig|Optional JSON object with device specific configuration. The JSON object will contain one or more key/value pairs. The key is a parameter for the device and the type of the value will vary.|
-
-The following is a list of the product keys and associated devices.
-These have been tested and should work out of the box:
-
-| Model | Description | Product Key | tested by |
-|-------|-------------|-------------|-----------|
-| 2477D | SwitchLinc Dimmer | F00.00.01 | Bernd Pfrommer |
-| 2477S | SwitchLinc Switch | F00.00.02 | Bernd Pfrommer |
-| 2845-222 | Hidden Door Sensor | F00.00.03 | Josenivaldo Benito |
-| 2876S | ICON Switch | F00.00.04 | Patrick Giasson |
-| 2456D3 | LampLinc V2 | F00.00.05 | Patrick Giasson |
-| 2442-222 | Micro Dimmer | F00.00.06 | Josenivaldo Benito |
-| 2453-222 | DIN Rail On/Off | F00.00.07 | Josenivaldo Benito |
-| 2452-222 | DIN Rail Dimmer | F00.00.08 | Josenivaldo Benito |
-| 2458-A1 | MorningLinc RF Lock Controller | F00.00.09 | cdeadlock |
-| 2852-222 | Leak Sensor | F00.00.0A | Kirk McCann |
-| 2672-422 | LED Dimmer | F00.00.0B | ??? |
-| 2476D | SwitchLinc Dimmer | F00.00.0C | LiberatorUSA |
-| 2634-222 | On/Off Dual-Band Outdoor Module | F00.00.0D | LiberatorUSA |
-| 2342-2 | Mini Remote | F00.00.10 | Bernd Pfrommer |
-| 2663-222 | On/Off Outlet | 0x000039 | SwissKid |
-| 2466D | ToggleLinc Dimmer | F00.00.11 | Rob Nielsen |
-| 2466S | ToggleLinc Switch | F00.00.12 | Rob Nielsen |
-| 2672-222 | LED Bulb | F00.00.13 | Rob Nielsen |
-| 2487S | KeypadLinc On/Off 6-Button | F00.00.14 | Bernd Pfrommer |
-| 2334-232 | KeypadLink Dimmer 6-Button | F00.00.15 | Rob Nielsen |
-| 2334-232 | KeypadLink Dimmer 8-Button | F00.00.16 | Rob Nielsen |
-| 2423A1 | iMeter Solo Power Meter | F00.00.17 | Rob Nielsen |
-| 2423A1 | Thermostat 2441TH | F00.00.18 | Daniel Campbell, Bernd Pfrommer |
-| 2457D2 | LampLinc Dimmer | F00.00.19 | Jonathan Huizingh |
-| 2475SDB | In-LineLinc Relay | F00.00.1A | Jim Howard |
-| 2635-222 | On/Off Module | F00.00.1B | Jonathan Huizingh |
-| 2475F | FanLinc Module | F00.00.1C | Brian Tillman |
-| 2456S3 | ApplianceLinc | F00.00.1D | ??? |
-| 2674-222 | LED Bulb (recessed) | F00.00.1E | Steve Bate |
-| 2477SA1 | 220V 30-amp Load Controller N/O | F00.00.1F | Shawn R. |
-| 2342-222 | Mini Remote (8 Button) | F00.00.20 | Bernd Pfrommer |
-| 2441V | Insteon Thermostat Adaptor for Venstar | F00.00.21 | Bernd Pfrommer |
-| 2982-222 | Insteon Smoke Bridge | F00.00.22 | Bernd Pfrommer |
-| 2487S | KeypadLinc On/Off 8-Button | F00.00.23 | Tom Weichmann |
-| 2450 | IO Link | 0x00001A | Bernd Pfrommer |
-| 2486D | KeypadLinc Dimmer | 0x000037 | Patrick Giasson, Joe Barnum |
-| 2484DWH8 | KeypadLinc Countdown Timer | 0x000041 | Rob Nielsen |
-| Various | PLM or hub | 0x000045 | Bernd Pfrommer |
-| 2843-222 | Wireless Open/Close Sensor | 0x000049 | Josenivaldo Benito |
-| 2842-222 | Motion Sensor | 0x00004A | Bernd Pfrommer |
-| 2844-222 | Motion Sensor II | F00.00.24 | Rob Nielsen |
-| 2486DWH8 | KeypadLinc Dimmer | 0x000051 | Chris Graham |
-| 2472D | OutletLincDimmer | 0x000068 | Chris Graham |
-| X10 switch | generic X10 switch | X00.00.01 | Bernd Pfrommer |
-| X10 dimmer | generic X10 dimmer | X00.00.02 | Bernd Pfrommer |
-| X10 motion | generic X10 motion sensor | X00.00.03 | Bernd Pfrommer |
+
+ Port configuration examples
+
+ | Modem Type | Port Configuration |
+ | --------------------- | --------------------------------------------------------------------------------------------------------------- |
+ | Hub (2245-222) | `/hub2/my_user_name:my_password@192.168.1.100:25105,poll_time=1000` |
+ | Legacy Hub (2242-222) | `/hub/192.168.1.100:9761` |
+ | PLM | `/dev/ttyS0` or `/dev/ttyUSB0` (Linux)
`COM1` (Windows)
`/tcp/192.168.1.100:9761` (Networked via ser2net) |
+ | Smartenit ZBPLM | `/dev/ttyUSB0,baudRate=115200` (Linux) |
+
+
+### `legacy-device`
+
+| Parameter | Required | Description |
+| ------------ | :------: | ------------------------------------------------------------ |
+| address | Yes | Device address. Example: `12.34.56` (Insteon) or `A.1` (X10) |
+| productKey | Yes | Product key used to identify the model of the device. |
+| deviceConfig | No | Optional JSON object with device specific configuration. |
+
+
+ Supported product keys
+
+ | Model | Description | Product Key |
+ | ---------- | -------------------------------------- | ----------- |
+ | 2477D | SwitchLinc Dimmer | F00.00.01 |
+ | 2477S | SwitchLinc Switch | F00.00.02 |
+ | 2845-222 | Hidden Door Sensor | F00.00.03 |
+ | 2876S | ICON Switch | F00.00.04 |
+ | 2456D3 | LampLinc V2 | F00.00.05 |
+ | 2442-222 | Micro Dimmer | F00.00.06 |
+ | 2453-222 | DIN Rail On/Off | F00.00.07 |
+ | 2452-222 | DIN Rail Dimmer | F00.00.08 |
+ | 2458-A1 | MorningLinc RF Lock Controller | F00.00.09 |
+ | 2852-222 | Leak Sensor | F00.00.0A |
+ | 2672-422 | LED Dimmer | F00.00.0B |
+ | 2476D | SwitchLinc Dimmer | F00.00.0C |
+ | 2634-222 | On/Off Dual-Band Outdoor Module | F00.00.0D |
+ | 2342-2 | Mini Remote | F00.00.10 |
+ | 2663-222 | On/Off Outlet | 0x000039 |
+ | 2466D | ToggleLinc Dimmer | F00.00.11 |
+ | 2466S | ToggleLinc Switch | F00.00.12 |
+ | 2672-222 | LED Bulb | F00.00.13 |
+ | 2487S | KeypadLinc On/Off 6-Button | F00.00.14 |
+ | 2334-232 | KeypadLink Dimmer 6-Button | F00.00.15 |
+ | 2334-232 | KeypadLink Dimmer 8-Button | F00.00.16 |
+ | 2423A1 | iMeter Solo Power Meter | F00.00.17 |
+ | 2423A1 | Thermostat 2441TH | F00.00.18 |
+ | 2457D2 | LampLinc Dimmer | F00.00.19 |
+ | 2475SDB | In-LineLinc Relay | F00.00.1A |
+ | 2635-222 | On/Off Module | F00.00.1B |
+ | 2475F | FanLinc Module | F00.00.1C |
+ | 2456S3 | ApplianceLinc | F00.00.1D |
+ | 2674-222 | LED Bulb (recessed) | F00.00.1E |
+ | 2477SA1 | 220V 30-amp Load Controller N/O | F00.00.1F |
+ | 2342-222 | Mini Remote (8 Button) | F00.00.20 |
+ | 2441V | Insteon Thermostat Adaptor for Venstar | F00.00.21 |
+ | 2982-222 | Insteon Smoke Bridge | F00.00.22 |
+ | 2487S | KeypadLinc On/Off 8-Button | F00.00.23 |
+ | 2450 | IO Link | 0x00001A |
+ | 2486D | KeypadLinc Dimmer | 0x000037 |
+ | 2484DWH8 | KeypadLinc Countdown Timer | 0x000041 |
+ | Various | PLM or Hub | 0x000045 |
+ | 2843-222 | Wireless Open/Close Sensor | 0x000049 |
+ | 2842-222 | Motion Sensor | 0x00004A |
+ | 2844-222 | Motion Sensor II | F00.00.24 |
+ | 2486DWH8 | KeypadLinc Dimmer | 0x000051 |
+ | 2472D | OutletLincDimmer | 0x000068 |
+ | X10 switch | generic X10 switch | X00.00.01 |
+ | X10 dimmer | generic X10 dimmer | X00.00.02 |
+ | X10 motion | generic X10 motion sensor | X00.00.03 |
+
+
## Channels
Below is the list of possible channels for the Insteon devices.
-In order to determine which channels a device supports, you can look at the device in the UI, or with the command `display_devices` in the console.
-
-| channel | type | description |
-|----------|--------|------------------------------|
-| acDelay | Number | AC Delay |
-| backlightDuration | Number | Back Light Duration |
-| batteryLevel | Number | Battery Level |
-| batteryPercent | Number:Dimensionless | Battery Percent |
-| batteryWatermarkLevel | Number | Battery Watermark Level |
-| beep | Switch | Beep |
-| bottomOutlet | Switch | Bottom Outlet |
-| buttonA | Switch | Button A |
-| buttonB | Switch | Button B |
-| buttonC | Switch | Button C |
-| buttonD | Switch | Button D |
-| buttonE | Switch | Button E |
-| buttonF | Switch | Button F |
-| buttonG | Switch | Button G |
-| buttonH | Switch | Button H |
-| broadcastOnOff | Switch | Broadcast On/Off |
-| contact | Contact | Contact |
-| coolSetPoint | Number | Cool Set Point |
-| dimmer | Dimmer | Dimmer |
-| fan | Number | Fan |
-| fanMode | Number | Fan Mode |
-| fastOnOff | Switch | Fast On/Off |
-| fastOnOffButtonA | Switch | Fast On/Off Button A |
-| fastOnOffButtonB | Switch | Fast On/Off Button B |
-| fastOnOffButtonC | Switch | Fast On/Off Button C |
-| fastOnOffButtonD | Switch | Fast On/Off Button D |
-| heatSetPoint | Number | Heat Set Point |
-| humidity | Number | Humidity |
-| humidityHigh | Number | Humidity High |
-| humidityLow | Number | Humidity Low |
-| isCooling | Number | Is Cooling |
-| isHeating | Number | Is Heating |
-| keypadButtonA | Switch | Keypad Button A |
-| keypadButtonB | Switch | Keypad Button B |
-| keypadButtonC | Switch | Keypad Button C |
-| keypadButtonD | Switch | Keypad Button D |
-| keypadButtonE | Switch | Keypad Button E |
-| keypadButtonF | Switch | Keypad Button F |
-| keypadButtonG | Switch | Keypad Button G |
-| keypadButtonH | Switch | Keypad Button H |
-| kWh | Number:Energy | Kilowatt Hour |
-| lastHeardFrom | DateTime | Last Heard From |
-| ledBrightness | Number | LED brightness |
-| ledOnOff | Switch | LED On/Off |
-| lightDimmer | Dimmer | light Dimmer |
-| lightLevel | Number | Light Level |
-| lightLevelAboveThreshold | Contact | Light Level Above/Below Threshold |
-| loadDimmer | Dimmer | Load Dimmer |
-| loadSwitch | Switch | Load Switch |
-| loadSwitchFastOnOff | Switch | Load Switch Fast On/Off |
-| loadSwitchManualChange | Number | Load Switch Manual Change |
-| lowBattery | Contact | Low Battery |
-| manualChange | Number | Manual Change |
-| manualChangeButtonA | Number | Manual Change Button A |
-| manualChangeButtonB | Number | Manual Change Button B |
-| manualChangeButtonC | Number | Manual Change Button C |
-| manualChangeButtonD | Number | Manual Change Button D |
-| notification | Number | Notification |
-| onLevel | Number | On Level |
-| rampDimmer | Dimmer | Ramp Dimmer |
-| rampRate | Number | Ramp Rate |
-| reset | Switch | Reset |
-| stage1Duration | Number | Stage 1 Duration |
-| switch | Switch | Switch |
-| systemMode | Number | System Mode |
-| tamperSwitch | Contact | Tamper Switch |
-| temperature | Number:Temperature | Temperature |
-| temperatureLevel | Number | Temperature Level |
-| topOutlet | Switch | Top Outlet |
-| update | Switch | Update |
-| watts | Number:Power | Watts |
+In order to determine which channels a device supports, check the device in the UI, or use the `insteon device listAll` console command.
+
+### State Channels
+
+| Channel | Type | Access Mode | Description |
+| --------------------- | -------------------- | :---------: | ---------------------------- |
+| 3-way-mode | Switch | R/W | 3-Way Toggle Mode |
+| ac-delay | Number:Time | R/W | AC Delay |
+| alert-delay | Switch | R/W | Alert Delay |
+| alert-duration | Number:Time | R/W | Alert Duration |
+| alert-type | String | R/W | Alert Type |
+| armed | Switch | R/W | Armed |
+| backlight-duration | Number:Time | R/W | Back Light Duration |
+| battery-level | Number:Dimensionless | R | Battery Level |
+| battery-powered | Switch | R | Battery Powered |
+| beep | Switch | W | Beep |
+| button-a | Switch | R/W | Button A |
+| button-b | Switch | R/W | Button B |
+| button-c | Switch | R/W | Button C |
+| button-d | Switch | R/W | Button D |
+| button-e | Switch | R/W | Button E |
+| button-f | Switch | R/W | Button F |
+| button-g | Switch | R/W | Button G |
+| button-h | Switch | R/W | Button H |
+| button-beep | Switch | R/W | Beep on Button Press |
+| button-config | String | R/W | Button Config |
+| button-lock | Switch | R/W | Button Lock |
+| carbon-monoxide-alarm | Switch | R | Carbon Monoxide Alarm |
+| contact | Contact | R | Contact Sensor |
+| cool-setpoint | Number:Temperature | R/W | Cool Setpoint |
+| daytime | Switch | R | Daytime |
+| dehumidify-setpoint | Number:Dimensionless | R/W | Dehumidify Setpoint |
+| dimmer | Dimmer | R/W | Dimmer |
+| energy-offset | Number:Temperature | R/W | Energy Temperature Offset |
+| energy-reset | Switch | W | Energy Usage Reset |
+| energy-saving | Switch | R | Energy Saving Mode |
+| energy-usage | Number:Energy | R | Energy Usage |
+| fan-mode | String | R/W | Fan Mode |
+| fan-speed | String | R/W | Fan Speed |
+| fan-state | Switch | R | Fan State |
+| fast-on-off | Switch | W | Fast On/Off |
+| heartbeat-interval | Number:Time | R/W | Heartbeat Interval |
+| heartbeat-on-off | Switch | R/W | Heartbeat Enabled |
+| heat-setpoint | Number:Temperature | R/W | Heat Setpoint |
+| humidifier-state | String | R | Humidifier State |
+| humidify-setpoint | Number:Dimensionless | R/W | Humidify Setpoint |
+| humidity | Number:Dimensionless | R | Ambient Humidity |
+| last-heard-from | DateTime | R | Last Heard From |
+| leak | Switch | R | Leak Sensor |
+| led-brightness | Dimmer | R/W | LED Brightness Level |
+| led-on-off | Switch | R/W | LED Enabled |
+| led-traffic | Switch | R/W | LED Traffic Blinking |
+| light-level | Number:Dimensionless | R | Ambient Light Level |
+| load | Switch | R | Load Sensor |
+| load-sense | Switch | R/W | Load Sense |
+| load-sense-bottom | Switch | R/W | Load Sense Bottom Outlet |
+| load-sense-top | Switch | R/W | Load Sense Top Outlet |
+| lock | Switch | R/W | Lock |
+| low-battery | Switch | R | Low Battery Alert |
+| malfunction | Switch | R | Malfunction Alert |
+| manual-change | Rollershutter | W | Manual Change |
+| momentary-duration | Number:Time | R/W | Momentary Duration |
+| monitor-mode | Switch | R/W | Monitor Mode |
+| motion | Switch | R | Motion Sensor |
+| on-level | Dimmer | R/W | On Level |
+| operation-mode | String | R/W | Switch Operation Mode |
+| outlet-bottom | Switch | R/W | Bottom Outlet |
+| outlet-top | Switch | R/W | Top Outlet |
+| power-usage | Number:Power | R | Power Usage |
+| program1 | Player | R/W | Program 1 |
+| program2 | Player | R/W | Program 2 |
+| program3 | Player | R/W | Program 3 |
+| program4 | Player | R/W | Program 4 |
+| program-lock | Switch | R/W | Local Programming Lock |
+| pump | Switch | R/W | Pump Control |
+| ramp-rate | Number:Time | R/W | Ramp Rate |
+| relay-mode | String | R/W | Output Relay Mode |
+| relay-sensor-follow | Switch | R/W | Output Relay Sensor Follow |
+| resume-dim | Switch | R/W | Resume Dim Level |
+| reverse-direction | Switch | R/W | Reverse Motor Direction |
+| rollershutter | Rollershutter | R/W | Rollershutter |
+| scene | Switch | R/W | Scene |
+| siren | Switch | R/W | Siren |
+| smoke-alarm | Switch | R | Smoke Alarm |
+| stage1-duration | Number:Time | R/W | Stage 1 Duration |
+| stay-awake | Switch | R/W | Stay Awake for Extended Time |
+| switch | Switch | R/W | Switch |
+| sync-time | Switch | W | Synchronize Time |
+| system-mode | String | R/W | System Mode |
+| system-state | String | R | System State |
+| tamper-switch | Contact | R | Tamper Switch |
+| temperature | Number:Temperature | R | Ambient Temperature |
+| temperature-scale | String | R/W | Temperature Scale |
+| test-alarm | Switch | R | Test Alarm |
+| time-format | String | R/W | Time Format |
+| toggle-mode-button-a | String | R/W | Toggle Mode Button A |
+| toggle-mode-button-b | String | R/W | Toggle Mode Button B |
+| toggle-mode-button-c | String | R/W | Toggle Mode Button C |
+| toggle-mode-button-d | String | R/W | Toggle Mode Button D |
+| toggle-mode-button-e | String | R/W | Toggle Mode Button E |
+| toggle-mode-button-f | String | R/W | Toggle Mode Button F |
+| toggle-mode-button-g | String | R/W | Toggle Mode Button G |
+| toggle-mode-button-h | String | R/W | Toggle Mode Button H |
+| valve1 | Switch | R/W | Valve 1 |
+| valve2 | Switch | R/W | Valve 2 |
+| valve3 | Switch | R/W | Valve 3 |
+| valve4 | Switch | R/W | Valve 4 |
+| valve5 | Switch | R/W | Valve 5 |
+| valve6 | Switch | R/W | Valve 6 |
+| valve7 | Switch | R/W | Valve 7 |
+| valve8 | Switch | R/W | Valve 8 |
+
+### Trigger Channels
+
+| Channel | Description |
+| ------------------- | ------------------- |
+| event-button | Event Button |
+| event-button-a | Event Button A |
+| event-button-b | Event Button B |
+| event-button-c | Event Button C |
+| event-button-d | Event Button D |
+| event-button-e | Event Button E |
+| event-button-f | Event Button F |
+| event-button-g | Event Button G |
+| event-button-h | Event Button H |
+| event-button-main | Event Button Main |
+| event-button-bottom | Event Button Bottom |
+| event-button-top | Event Button Top |
+| im-event-button | Event Button |
+
+The supported triggered events for Insteon Device things:
+
+| Event | Description |
+| -------------------- | ------------------------------------- |
+| `PRESSED_ON` | Button Pressed On (Regular On) |
+| `PRESSED_OFF` | Button Pressed Off (Regular Off) |
+| `DOUBLE_PRESSED_ON` | Button Double Pressed On (Fast On) |
+| `DOUBLE_PRESSED_OFF` | Button Double Pressed Off (Fast Off) |
+| `HELD_UP` | Button Held Up (Manual Change Up) |
+| `HELD_DOWN` | Button Held Down (Manual Change Down) |
+| `RELEASED` | Button Released (Manual Change Stop) |
+
+And for Insteon Hub and PLM things:
+
+| Event | Description |
+| ---------- | --------------- |
+| `PRESSED` | Button Pressed |
+| `HELD` | Button Held |
+| `RELEASED` | Button Released |
+
+### Legacy Channels
+
+
+
+ | Channel | Type | Description |
+ | ------------------------ | -------------------- | --------------------------------- |
+ | acDelay | Number | AC Delay |
+ | backlightDuration | Number | Back Light Duration |
+ | batteryLevel | Number | Battery Level |
+ | batteryPercent | Number:Dimensionless | Battery Percent |
+ | batteryWatermarkLevel | Number | Battery Watermark Level |
+ | beep | Switch | Beep |
+ | bottomOutlet | Switch | Bottom Outlet |
+ | buttonA | Switch | Button A |
+ | buttonB | Switch | Button B |
+ | buttonC | Switch | Button C |
+ | buttonD | Switch | Button D |
+ | buttonE | Switch | Button E |
+ | buttonF | Switch | Button F |
+ | buttonG | Switch | Button G |
+ | buttonH | Switch | Button H |
+ | broadcastOnOff | Switch | Broadcast On/Off |
+ | contact | Contact | Contact |
+ | coolSetPoint | Number | Cool Setpoint |
+ | dimmer | Dimmer | Dimmer |
+ | fan | Number | Fan |
+ | fanMode | Number | Fan Mode |
+ | fastOnOff | Switch | Fast On/Off |
+ | fastOnOffButtonA | Switch | Fast On/Off Button A |
+ | fastOnOffButtonB | Switch | Fast On/Off Button B |
+ | fastOnOffButtonC | Switch | Fast On/Off Button C |
+ | fastOnOffButtonD | Switch | Fast On/Off Button D |
+ | heatSetPoint | Number | Heat Setpoint |
+ | humidity | Number | Humidity |
+ | humidityHigh | Number | Humidity High |
+ | humidityLow | Number | Humidity Low |
+ | isCooling | Number | Is Cooling |
+ | isHeating | Number | Is Heating |
+ | keypadButtonA | Switch | Keypad Button A |
+ | keypadButtonB | Switch | Keypad Button B |
+ | keypadButtonC | Switch | Keypad Button C |
+ | keypadButtonD | Switch | Keypad Button D |
+ | keypadButtonE | Switch | Keypad Button E |
+ | keypadButtonF | Switch | Keypad Button F |
+ | keypadButtonG | Switch | Keypad Button G |
+ | keypadButtonH | Switch | Keypad Button H |
+ | kWh | Number:Energy | Kilowatt Hour |
+ | lastHeardFrom | DateTime | Last Heard From |
+ | ledBrightness | Number | LED brightness |
+ | ledOnOff | Switch | LED On/Off |
+ | lightDimmer | Dimmer | light Dimmer |
+ | lightLevel | Number | Light Level |
+ | lightLevelAboveThreshold | Contact | Light Level Above/Below Threshold |
+ | loadDimmer | Dimmer | Load Dimmer |
+ | loadSwitch | Switch | Load Switch |
+ | loadSwitchFastOnOff | Switch | Load Switch Fast On/Off |
+ | loadSwitchManualChange | Number | Load Switch Manual Change |
+ | lowBattery | Contact | Low Battery |
+ | manualChange | Number | Manual Change |
+ | manualChangeButtonA | Number | Manual Change Button A |
+ | manualChangeButtonB | Number | Manual Change Button B |
+ | manualChangeButtonC | Number | Manual Change Button C |
+ | manualChangeButtonD | Number | Manual Change Button D |
+ | notification | Number | Notification |
+ | onLevel | Number | On Level |
+ | rampDimmer | Dimmer | Ramp Dimmer |
+ | rampRate | Number | Ramp Rate |
+ | reset | Switch | Reset |
+ | stage1Duration | Number | Stage 1 Duration |
+ | switch | Switch | Switch |
+ | systemMode | Number | System Mode |
+ | tamperSwitch | Contact | Tamper Switch |
+ | temperature | Number:Temperature | Temperature |
+ | temperatureLevel | Number | Temperature Level |
+ | topOutlet | Switch | Top Outlet |
+ | update | Switch | Update |
+ | watts | Number:Power | Watts |
+
+
## Full Example
-Sample things file:
+### Things
```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device 22F8A8 [address="22.F8.A8", productKey="F00.00.15"] {
- Channels:
- Type keypadButtonA : keypadButtonA [ group=3 ]
- Type keypadButtonB : keypadButtonB [ group=4 ]
- Type keypadButtonC : keypadButtonC [ group=5 ]
- Type keypadButtonD : keypadButtonD [ group=6 ]
- }
- Thing device 238D93 [address="23.8D.93", productKey="F00.00.12"]
- Thing device 238F55 [address="23.8F.55", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [related="23.B0.D9+23.8F.C9"]
- }
- Thing device 238FC9 [address="23.8F.C9", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [related="23.8F.55+23.B0.D9"]
- }
- Thing device 23B0D9 [address="23.B0.D9", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [related="23.8F.55+23.8F.C9"]
- }
- Thing device 243141 [address="24.31.41", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [dimmermax=60]
- }
+Bridge insteon:plm:home [serialPort="/dev/ttyUSB0"] {
+ Thing device 22f8a8 [address="22.F8.A8"]
+ Thing device 238d93 [address="23.8D.93"]
+ Thing device 238f55 [address="23.8F.55"]
+ Thing device 238fc9 [address="23.8F.C9"]
+ Thing device 23b0d9 [address="23.B0.D9"]
+ Thing scene scene42 [group=42]
+ Thing x10 a2 [houseCode="A", unitCode=2, deviceType="X10_Switch"]
}
```
-Sample items file:
+
+ Legacy
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device 22F8A8 [address="22.F8.A8", productKey="F00.00.15"] {
+ Channels:
+ Type keypadButtonA : keypadButtonA [ group=3 ]
+ Type keypadButtonB : keypadButtonB [ group=4 ]
+ Type keypadButtonC : keypadButtonC [ group=5 ]
+ Type keypadButtonD : keypadButtonD [ group=6 ]
+ }
+ Thing device 238D93 [address="23.8D.93", productKey="F00.00.12"]
+ Thing device 238F55 [address="23.8F.55", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [related="23.B0.D9+23.8F.C9"]
+ }
+ Thing device 238FC9 [address="23.8F.C9", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [related="23.8F.55+23.B0.D9"]
+ }
+ Thing device 23B0D9 [address="23.B0.D9", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [related="23.8F.55+23.8F.C9"]
+ }
+ Thing device 243141 [address="24.31.41", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [dimmermax=60]
+ }
+ }
+ ```
+
+
+
+### Items
```java
Switch switch1 { channel="insteon:device:home:243141:switch" }
-Dimmer dimmer1 { channel="insteon:device:home:238F55:dimmer" }
-Dimmer dimmer2 { channel="insteon:device:home:23B0D9:dimmer" }
-Dimmer dimmer3 { channel="insteon:device:home:238FC9:dimmer" }
-Dimmer keypad { channel="insteon:device:home:22F8A8:loadDimmer" }
-Switch keypadA { channel="insteon:device:home:22F8A8:keypadButtonA" }
-Switch keypadB { channel="insteon:device:home:22F8A8:keypadButtonB" }
-Switch keypadC { channel="insteon:device:home:22F8A8:keypadButtonC" }
-Switch keypadD { channel="insteon:device:home:22F8A8:keypadButtonD" }
-Dimmer dimmer { channel="insteon:device:home:238D93:dimmer" }
+Dimmer dimmer1 { channel="insteon:device:home:238f55:dimmer" }
+Dimmer dimmer2 { channel="insteon:device:home:23b0d9:dimmer" }
+Dimmer dimmer3 { channel="insteon:device:home:238fc9:dimmer" }
+Dimmer keypad { channel="insteon:device:home:22f8a8:dimmer" }
+Switch keypadA { channel="insteon:device:home:22f8a8:button-a" }
+Switch keypadB { channel="insteon:device:home:22f8a8:button-b" }
+Switch keypadC { channel="insteon:device:home:22f8a8:button-c" }
+Switch keypadD { channel="insteon:device:home:22f8a8:button-d" }
+Switch scene42 { channel="insteon:scene:home:scene42:scene" }
+Switch switch2 { channel="insteon:x10:home:a2:switch" }
```
## Console Commands
-The binding provides commands you can use to help with troubleshooting.
-Enter `openhab:insteon` or `insteon` in the console and you will get a list of available commands.
-The `openhab:` prefix is optional:
+The binding provides commands to help with configuring and troubleshooting.
+Most commands support auto-completion during input based on the existing configuration.
+If a legacy network bridge is active, the console will revert to legacy commands.
+Enter `openhab:insteon` or `insteon` in the console to get a list of available commands.
```shell
-openhab> openhab:insteon
-Usage: openhab:insteon display_devices - display devices that are online, along with available channels
-Usage: openhab:insteon display_channels - display channels that are linked, along with configuration information
-Usage: openhab:insteon display_local_database - display Insteon PLM or hub database details
-Usage: openhab:insteon display_monitored - display monitored device(s)
-Usage: openhab:insteon start_monitoring all|address - start displaying messages received from device(s)
-Usage: openhab:insteon stop_monitoring all|address - stop displaying messages received from device(s)
-Usage: openhab:insteon send_standard_message address flags cmd1 cmd2 - send standard message to a device
-Usage: openhab:insteon send_extended_message address flags cmd1 cmd2 [up to 13 bytes] - send extended message to a device
-Usage: openhab:insteon send_extended_message_2 address flags cmd1 cmd2 [up to 12 bytes] - send extended message with a two byte crc to a device
+openhab> insteon
+Usage: openhab:insteon modem - Insteon modem commands
+Usage: openhab:insteon device - Insteon/X10 device commands
+Usage: openhab:insteon scene - Insteon scene commands
+Usage: openhab:insteon channel - Insteon channel commands
+Usage: openhab:insteon debug - Insteon debug commands
```
-Here is an example of command: `insteon display_local_database`.
+
+ Legacy
-The send message commands do not display any results.
-If you want to see the response from the device, you will need to monitor the device.
+ ```shell
+ openhab> insteon
+ Usage: openhab:insteon display_devices - display devices that are online, along with available channels
+ Usage: openhab:insteon display_channels - display channels that are linked, along with configuration information
+ Usage: openhab:insteon display_local_database - display Insteon PLM or hub database details
+ Usage: openhab:insteon display_monitored - display monitored device(s)
+ Usage: openhab:insteon start_monitoring all|address - start displaying messages received from device(s)
+ Usage: openhab:insteon stop_monitoring all|address - stop displaying messages received from device(s)
+ Usage: openhab:insteon send_standard_message address flags cmd1 cmd2 - send standard message to a device
+ Usage: openhab:insteon send_extended_message address flags cmd1 cmd2 [up to 13 bytes] - send extended message to a device
+ Usage: openhab:insteon send_extended_message_2 address flags cmd1 cmd2 [up to 12 bytes] - send extended message with a two byte crc to a device
+ ```
+
+
## Insteon Groups and Scenes
@@ -268,93 +557,303 @@ How do Insteon devices tell other devices on the network that their state has ch
All devices (called _responders_) that are configured to listen to this message will then go into a pre-defined state.
For instance when light switch A is switched to "ON", it will send out a message to group #1, and all responders will react to it, e.g they may go into the "ON" position as well.
Since more than one device can participate, the sending out of the broadcast message and the subsequent state change of the responders is referred to as "triggering a scene".
-At the device and PLM level, the concept of a "scene" does not exist, so you will find it notably absent in the binding code and this document.
-A scene is strictly a higher level concept, introduced to shield the user from the details of how the communication is implemented.
Many Insteon devices send out messages on different group numbers, depending on what happens to them.
A leak sensor may send out a message on group #1 when dry, and on group #2 when wet.
The default group used for e.g. linking two light switches is usually group #1.
+The binding can now automatically determines the broadcast groups between the modem and linked devices, based on their all-link databases.
+
+By default, the binding only sends direct messages to the intended device to update its state, leaving the state of the related devices unchanged.
+Whenever the bridge related device synchronization parameter `deviceSyncEnabled` is set to `true`, broadcast messages for supported Insteon commands (e.g. on/off, bright/dim, manual change) are sent to all responders of a given group, updating all related devices in one request.
+If no broadcast group is determined or for Insteon commands that don't support broadcasting (e.g. percent), direct messages are sent to each related device instead, to adjust their level based on their all-link database.
+
## Insteon Binding Process
Before Insteon devices communicate with one another, they must be linked.
-During the linking process, one of the devices will be the "Controller", the other the "Responder" (see e.g. the [SwitchLinc Instructions](https://www.insteon.com/pdf/2477S.pdf)).
+During the linking process, one of the devices will be the "Controller", the other the "Responder".
The responder listens to messages from the controller, and reacts to them.
-Note that except for the case of a motion detector (which is just a controller to the modem), the modem controls the device (e.g. send on/off messages to it), and the device controls the modem (so the modem learns about the switch being toggled.
+Note that except for the case of a motion detector (which is just a controller to the modem), the modem controls the device (e.g. send on/off messages to it), and the device controls the modem, so it learns about the switch being toggled.
For this reason, most devices and in particular switches/dimmers should be linked twice, with one taking the role of controller during the first linking, and the other acting as controller during the second linking process.
To do so, first press and hold the "Set" button on the modem until the light starts blinking.
-Then press and hold the "Set" button on the remote device,
-e.g. the light switch, until it double beeps (the light on the modem should go off as well.
+Then press and hold the "Set" button on the remote device, e.g. the light switch, until it double beeps (the light on the modem should go off as well).
Now do exactly the reverse: press and hold the "Set" button on the remote device until its light starts blinking, then press and hold the "Set" button on the modem until it double beeps, and the light of the remote device (switch) goes off.
-For some of the more sophisticated devices the complete linking process can no longer be done with the set buttons, but requires software like [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal).
+Alternatively, the binding can link a device to the modem programmatically using the `insteon modem addDevice` console command.
+Based on the initial set button pressed event received, the device will be linked one or both ways.
+Once the newly linked device is added as a thing, additional links for more complex devices can be added using the `insteon device addMissingLinks` console command.
-## Insteon Features
+## Insteon Devices
-Since Insteon devices can have multiple features (for instance a switchable relay and a contact sensor) under a single Insteon address, an openHAB item is not bound to a device, but to a given feature of a device.
+Since Insteon devices can have multiple features (for instance a switchable relay and a contact sensor) under a single Insteon device, an openHAB item is not bound to a device, but to a given feature of a device.
For example, the following lines would create two Number items referring to the same thermostat device, but to different features of it:
```java
-Number thermostatCoolPoint "cool point [%.1f °F]" { channel="insteon:device:home:32F422:coolSetPoint" }
-Number thermostatHeatPoint "heat point [%.1f °F]" { channel="insteon:device:home:32F422:heatSetPoint" }
+Number:Temperature thermostatCoolSetpoint "cool setpoint [%.1f °F]" { channel="insteon:device:home:32f422:cool-setpoint" }
+Number:Temperature thermostatHeatSetpoint "heat setpoint [%.1f °F]" { channel="insteon:device:home:32f422:heat-setpoint" }
```
-### Simple Light Switches
+### Switches
The following example shows how to configure a simple light switch (2477S) in the .items file:
```java
-Switch officeLight "office light" { channel="insteon:device:home:AABBCC:switch" }
+Switch officeLight "office light" { channel="insteon:device:home:aabbcc:switch" }
```
-### Simple Dimmers
+### Dimmers
Here is how to configure a simple dimmer (2477D) in the .items file:
```java
-Dimmer kitchenChandelier "kitchen chandelier" { channel="insteon:device:home:AABBCC:dimmer" }
+Dimmer kitchenChandelier "kitchen chandelier" { channel="insteon:device:home:aabbcc:dimmer" }
```
-Dimmers can be configured with a maximum level when turning a device on or setting a percentage level.
-If a maximum level is configured, openHAB will never set the level of the dimmer above the level specified.
-The parameter dimmermax must be defined for the channel.
-The below example sets a maximum level of 70% for dim 1 and 60% for dim 2:
+For `ON` command requests, the binding uses the device on level and ramp rate local settings to set the dimmer level, the same way it would be set when physically pressing on the dimmer.
+These settings can be controlled using the `on-level` and `ramp-rate` channels.
-#### Things
+Alternatively, these settings can be overridden using the `dimmer` channel parameters `onLevel` and `rampRate`.
+Doing so will result in different type of commands being triggered as opposed to having separate channels previously such as `fastOnOff`, `manualChange` and `rampDimmer` handling it.
+
+When the `rampRate` parameter is configured, the binding will send a ramp rate command (previously triggered by the `rampDimmer` channel) to the relevant device to set the level at the defined ramp rate.
+When this parameter is set to instant (0.1 sec), on/off commands will trigger what used to be handled by the `fastOnOff` channel.
+And percent commands will trigger what is defined in the Insteon protocol as instant change requests.
+
+As far as the previously known `manualChange` channel, it has been rolled into the `rollershutter` channel for [window covering](#window-coverings) using `UP`, `DOWN` and `STOP` commands.
+For the `dimmer` channel, the `INCREASE` and `DECREASE` commands can be used instead.
+
+Ultimately, the `dimmer` channel parameters can be used to create custom channels via a thing file that can work as an alternative to having to configure an Insteon scene for a single device.
```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [dimmermax=70]
- }
- Thing device AABBCD [address="AA.BB.CD", productKey="F00.00.15"] {
- Channels:
- Type loadDimmer : loadDimmer [dimmermax=60]
+Thing device 23b0d9 [address="23.B0.D9"] {
+ Channels:
+ // 50% on level at 2.5 minutes ramp rate
+ Type dimmer : custom1 [onLevel=50, rampRate=150]
+ // 80% on level at device configured ramp rate
+ Type dimmer : custom2 [onLevel=80]
+ // device configured on level at 8 minutes ramp rate
+ Type dimmer : custom3 [rampRate=480]
+}
+```
+
+
+ Legacy
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [dimmermax=70]
+ }
+ Thing device AABBCD [address="AA.BB.CD", productKey="F00.00.15"] {
+ Channels:
+ Type loadDimmer : loadDimmer [dimmermax=60]
+ }
}
+ ```
+
+
+
+### Keypads
+
+The Insteon keypad devices typically control one main load and have a number of buttons that will send out group broadcast messages to trigger a scene.
+To use the main load switch within openHAB, link the modem and device with the set buttons as usual.
+For the scene buttons, each one will send out a message for a different, predefined group.
+The button numbering used internally by the device must be mapped to whatever labels are printed on the physical buttons of the device.
+Here is an example correspondence table:
+
+| Group | Button Number | 2487S Label |
+| :---: | :-----------: | :---------: |
+| 0x01 | 1 | (Load) |
+| 0x03 | 3 | A |
+| 0x04 | 4 | B |
+| 0x05 | 5 | C |
+| 0x06 | 6 | D |
+
+When e.g. the "A" button is pressed (that's button #3 internally) a broadcast message will be sent out to all responders configured to listen to Insteon group #3.
+In this case, the modem must be configured as a responder to group #3 (and #4, #5, #6) messages coming from the keypad.
+These groups can be linked programmatically using the `insteon device addMissingLinks` console command, or via the device set buttons (see the keypad instructions).
+
+While previously, keypad buttons required a broadcast group to be configured, the binding now automatically determines that setting, based on the device link databases, deprecating the `group` channel parameter.
+By default, the binding will only change the button led state when receiving on/off commands, depending on the keypad local radio group settings.
+For button broadcast group support, set the bridge parameter `deviceSyncEnabled` to `true`.
+Additionally, for button toggle mode set to always on or off, only `ON` or `OFF` commands will be processed, in line with the physical interaction.
+
+#### Keypad Switches
+
+##### Items
+
+The following items will expose a keypad switch and its associated buttons:
+
+```java
+Switch keypadSwitch "main switch" { channel="insteon:device:home:aabbcc:switch" }
+Switch keypadSwitchA "button A" { channel="insteon:device:home:aabbcc:button-a"}
+Switch keypadSwitchB "button B" { channel="insteon:device:home:aabbcc:button-b"}
+Switch keypadSwitchC "button C" { channel="insteon:device:home:aabbcc:button-c"}
+Switch keypadSwitchD "button D" { channel="insteon:device:home:aabbcc:button-d"}
+```
+
+
+ Legacy
+
+ ```java
+ Switch keypadSwitch "main switch" { channel="insteon:device:home:AABBCC:switch" }
+ Switch keypadSwitchA "button A" { channel="insteon:device:home:AABBCC:buttonA"}
+ Switch keypadSwitchB "button B" { channel="insteon:device:home:AABBCC:buttonB"}
+ Switch keypadSwitchC "button C" { channel="insteon:device:home:AABBCC:buttonC"}
+ Switch keypadSwitchD "button D" { channel="insteon:device:home:AABBCC:buttonD"}
+ ```
+
+
+
+##### Sitemap
+
+The following sitemap will bring the items to life in the GUI:
+
+```perl
+Frame label="Keypad" {
+ Switch item=keypadSwitch label="main"
+ Switch item=keypadSwitchA label="button A"
+ Switch item=keypadSwitchB label="button B"
+ Switch item=keypadSwitchC label="button C"
+ Switch item=keypadSwitchD label="button D"
}
```
-#### Items
+##### Rules
+
+The following rules will monitor regular on/off, fast on/off and manual change button events:
+
+```java
+rule "Main Button Off Event"
+when
+ Channel 'insteon:device:home:aabbcc:event-button-main' triggered PRESSED_OFF
+then
+ // do something
+end
+
+rule "Main Button Fast On/Off Events"
+when
+ Channel 'insteon:device:home:aabbcc:event-button-main' triggered DOUBLE_PRESSED_ON or
+ Channel 'insteon:device:home:aabbcc:event-button-main' triggered DOUBLE_PRESSED_OFF
+then
+ // do something
+end
+
+rule "Main Button Manual Change Stop Event"
+when
+ Channel 'insteon:device:home:aabbcc:event-button-main' triggered RELEASED
+then
+ // do something
+end
+
+rule "Keypad Button A On Event"
+when
+ Channel 'insteon:device:home:aabbcc:event-button-a' triggered PRESSED_ON
+then
+ // do something
+end
+```
+
+
+ Legacy
+
+##### Items
+
+ Here is a simple example, just using the load (main) switch:
+
+ ```java
+ Switch keypadSwitch "main load" { channel="insteon:device:home:AABBCC:loadSwitch" }
+ Number keypadSwitchManualChange "main manual change" { channel="insteon:device:home:AABBCC:loadSwitchManualChange" }
+ Switch keypadSwitchFastOnOff "main fast on/off" { channel="insteon:device:home:AABBCC:loadSwitchFastOnOff" }
+ Switch keypadSwitchA "keypad button A" { channel="insteon:device:home:AABBCC:keypadButtonA"}
+ Switch keypadSwitchB "keypad button B" { channel="insteon:device:home:AABBCC:keypadButtonB"}
+ Switch keypadSwitchC "keypad button C" { channel="insteon:device:home:AABBCC:keypadButtonC"}
+ Switch keypadSwitchD "keypad button D" { channel="insteon:device:home:AABBCC:keypadButtonD"}
+ ```
+
+##### Things
+
+ The value after group must either be a number or string.
+ The hexadecimal value 0xf3 can either converted to a numeric value 243 or the string value "0xf3".
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.15"] {
+ Channels:
+ Type keypadButtonA : keypadButtonA [ group="0xf3" ]
+ Type keypadButtonB : keypadButtonB [ group="0xf4" ]
+ Type keypadButtonC : keypadButtonC [ group="0xf5" ]
+ Type keypadButtonD : keypadButtonD [ group="0xf6" ]
+ }
+ }
+ ```
+
+##### Sitemap
+
+ The following sitemap will bring the items to life in the GUI:
+
+ ```perl
+ Frame label="Keypad" {
+ Switch item=keypadSwitch label="main"
+ Switch item=keypadSwitchFastOnOff label="fast on/off"
+ Switch item=keypadSwitchManualChange label="manual change" mappings=[ 0="DOWN", 1="STOP", 2="UP"]
+ Switch item=keypadSwitchA label="button A"
+ Switch item=keypadSwitchB label="button B"
+ Switch item=keypadSwitchC label="button C"
+ Switch item=keypadSwitchD label="button D"
+ }
+ ```
+
+
+
+#### Keypad Dimmers
+
+The keypad dimmers are like keypad switches, except that the main load is dimmable.
+
+##### Items
```java
-Dimmer d1 "dimmer 1" { channel="insteon:device:home:AABBCC:dimmer"}
-Dimmer d2 "dimmer 2" { channel="insteon:device:home:AABBCD:loadDimmer"}
+Dimmer keypadDimmer "main dimmer" { channel="insteon:device:home:aabbcc:dimmer" }
+Switch keypadDimmerButtonA "button A" { channel="insteon:device:home:aabbcc:button-a" }
```
-Setting a maximum level does not affect manual turning on or dimming a switch.
+
+ Legacy
+
+ ```java
+ Dimmer keypadDimmer "main dimmer" { channel="insteon:device:home:AABBCC:dimmer" }
+ Switch keypadDimmerButtonA "button A" { channel="insteon:device:home:AABBCC:buttonA" }
+ ```
+
+
+
+##### Sitemap
+
+```perl
+Slider item=keypadDimmer label="main" switchSupport
+Switch item=keypadDimmerButtonA label="button A"
+```
-### On/Off Outlets
+### Outlets
Here's how to configure the top and bottom outlet of the in-wall 2 outlet controller:
```java
-Switch fOutTop "Front Outlet Top" { channel="insteon:device:home:AABBCC:topOutlet" }
-Switch fOutBot "Front Outlet Bottom" { channel="insteon:device:home:AABBCC:bottomOutlet" }
+Switch outletTop "Outlet Top" { channel="insteon:device:home:aabbcc:outlet-top" }
+Switch outletBottom "Outlet Bottom" { channel="insteon:device:home:aabbcc:outlet-bottom" }
```
-This will give you individual control of each outlet.
+
+ Legacy
+
+ ```java
+ Switch outletTop "Outlet Top" { channel="insteon:device:home:AABBCC:topOutlet" }
+ Switch outletBottom "Outlet Bottom" { channel="insteon:device:home:AABBCC:bottomOutlet" }
+ ```
+
+
### Mini Remotes
@@ -370,82 +869,120 @@ The modem's link database (see [Insteon Terminal](https://github.com/pfrommerd/i
0000 xx.xx.xx xx.xx.xx RESP 10100010 group: 04 data: 02 2c 41
```
-**Items**
-This goes into the items file:
+The mini remote buttons cannot be modeled as items since they don't have a state or can receive commands. However, button triggered events can be monitored through rules that can set off subsequent actions:
+
+##### Rules
```java
-Switch miniRemoteButtonA "mini remote button a" { channel="insteon:device:home:AABBCC:buttonA", autoupdate="false" }
-Switch miniRemoteButtonB "mini remote button b" { channel="insteon:device:home:AABBCC:buttonB", autoupdate="false" }
-Switch miniRemoteButtonC "mini remote button c" { channel="insteon:device:home:AABBCC:buttonC", autoupdate="false" }
-Switch miniRemoteButtonD "mini remote button d" { channel="insteon:device:home:AABBCC:buttonD", autoupdate="false" }
+rule "Mini Remote Button A Pressed On"
+when
+ Channel 'insteon:device:home:mini-remote:event-button-a' triggered PRESSED_ON
+then
+ // do something
+end
```
-**Sitemap**
-This goes into the sitemap file:
+### Motion Sensors
-```perl
-Switch item=miniRemoteButtonA label="mini remote button a" mappings=[ OFF="Off", ON="On"]
-Switch item=miniRemoteButtonB label="mini remote button b" mappings=[ OFF="Off", ON="On"]
-Switch item=miniRemoteButtonC label="mini remote button c" mappings=[ OFF="Off", ON="On"]
-Switch item=miniRemoteButtonD label="mini remote button d" mappings=[ OFF="Off", ON="On"]
+Link such that the modem is a responder to the motion sensor.
+
+##### Items
+
+```java
+Switch motionSensor "motion sensor [MAP(motion.map):%s]" { channel="insteon:device:home:aabbcc:motion"}
+Number:Dimensionless motionSensorBatteryLevel "battery level [%.1f %%]" { channel="insteon:device:home:aabbcc:battery-level" }
+Number:Dimensionless motionSensorLightLevel "light level [%.1f %%]" { channel="insteon:device:home:aabbcc:light-level" }
```
-The switches in the GUI just display the mini remote's most recent button presses.
-They are not operable because the PLM cannot trigger the mini remotes scenes.
+
+ Legacy
-### Motion Sensors
+ ```java
+ Contact motionSensor "motion sensor [MAP(motion.map):%s]" { channel="insteon:device:home:AABBCC:contact"}
+ Number motionSensorBatteryLevel "motion sensor battery level" { channel="insteon:device:home:AABBCC:batteryLevel" }
+ Number motionSensorLightLevel "motion sensor light level" { channel="insteon:device:home:AABBCC:lightLevel" }
+ ```
-Link such that the modem is a responder to the motion sensor.
-Create a contact.map file in the transforms directory as described elsewhere in this document.
-Then create entries in the .items file like this:
+
-#### Items
+and create a file "motion.map" in the transforms directory with these entries:
-```java
-Contact motionSensor "motion sensor [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact"}
-Number motionSensorBatteryLevel "motion sensor battery level" { channel="insteon:device:home:AABBCC:batteryLevel" }
-Number motionSensorLightLevel "motion sensor light level" { channel="insteon:device:home:AABBCC:lightLevel" }
+```text
+ON=detected
+OFF=cleared
+-=unknown
```
-This will give you a contact, the battery level, and the light level.
-The motion sensor II includes three additional channels:
+The motion sensor II includes additional channels:
```java
-Number motionSensorBatteryPercent "motion sensor battery percent" { channel="insteon:device:home:AABBCC:batteryPercent" }
-Contact motionSensorTamperSwitch "motion sensor tamper switch [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:tamperSwitch"}
-Number motionSensorTemperatureLevel "motion sensor temperature level" { channel="insteon:device:home:AABBCC:temperatureLevel" }
+Contact motionSensorTamperSwitch "tamper switch [MAP(contact.map):%s]" { channel="insteon:device:home:aabbcc:tamper-switch" }
+Number:Temperature motionSensorTemperature "temperature [%.1f °F]" { channel="insteon:device:home:aabbcc:temperature" }
```
-The battery, light level and temperature level are updated when either there is motion, light level above/below threshold, tamper switch activated, or the sensor battery runs low.
-This is accomplished by querying the device for the data.
-The motion sensor II will also periodically send data if the alternate heartbeat is enabled on the device.
+
+ Legacy
-If the alternate heartbeat is enabled, the device can be configured to not query the device and rely on the data from the alternate heartbeat.
-Disabling the querying of the device should provide more accurate battery data since it appears to fluctuate with queries of the device.
-This can be configured with the device configuration parameter of the device.
-The key in the JSON object is `heartbeatOnly` and the value is a boolean:
+ ```java
+ Contact motionSensorTamperSwitch "tamper switch [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:tamperSwitch" }
+ Number:Temperature motionSensorTemperature "temperature [%.1f °F]" { channel="insteon:device:home:AABBCC:temperature" }
+ ```
-#### Things
+
-```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.24", deviceConfig="{'heartbeatOnly': true}"]
-}
+The temperature is automatically calculated in Fahrenheit based on the motion sensor II powered source.
+Since that sensor might not be calibrated correctly, the output temperature may need to be offset on the openHAB side.
-```
+The battery and light level are only updated when either there is motion, light level above/below threshold, tamper switch activated, or the sensor battery runs low.
+
+
+ Legacy
-The temperature can be calculated in Fahrenheit using the following formulas:
+ If the alternate heartbeat is enabled, the device can be configured to not query the device and rely on the data from the alternate heartbeat.
+ Disabling the querying of the device should provide more accurate battery data since it appears to fluctuate with queries of the device.
+ This can be configured with the device configuration parameter of the device.
+ The key in the JSON object is `heartbeatOnly` and the value is a boolean:
+
+#### Things
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.24", deviceConfig="{'heartbeatOnly': true}"]
+ }
+ ```
+
+ The temperature can be calculated in Fahrenheit using the following formulas:
- If the device is battery powered: `temperature = 0.73 * motionSensorTemperatureLevel - 20.53`
- If the device is USB powered: `temperature = 0.72 * motionSensorTemperatureLevel - 24.61`
-Since the motion sensor II might not be calibrated correctly, the values `20.53` and `24.61` can be adjusted as necessary to produce the correct temperature.
+ Since the motion sensor II might not be calibrated correctly, the values `20.53` and `24.61` can be adjusted as necessary to produce the correct temperature.
+
+
### Hidden Door Sensors
Similar in operation to the motion sensor above.
Link such that the modem is a responder to the motion sensor.
-Create a contact.map file in the transforms directory like the following:
+
+##### Items
+
+```java
+Contact doorSensor "door sensor [MAP(contact.map):%s]" { channel="insteon:device:home:aabbcc:contact" }
+Number:Dimensionless doorSensorBatteryLevel "battery level [%.1f %%]" { channel="insteon:device:home:aabbcc:battery-level" }
+```
+
+
+ Legacy
+
+ ```java
+ Contact doorSensor "door sensor [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact" }
+ Number:Dimensionless doorSensorBatteryLevel "battery level [%.1f %%]" { channel="insteon:device:home:AABBCC:batteryLevel" }
+ ```
+
+
+
+and create a file "contact.map" in the transforms directory with these entries:
```text
OPEN=open
@@ -453,33 +990,32 @@ CLOSED=closed
-=unknown
```
-**Items**
-Then create entries in the .items file like this:
-
-```java
-Contact doorSensor "Door sensor [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact" }
-Number doorSensorBatteryLevel "Door sensor battery level [%.1f]" { channel="insteon:device:home:AABBCC:batteryLevel" }
-```
-
-This will give you a contact and the battery level.
-Note that battery level is only updated when either there is motion, or the sensor battery runs low.
+Note that battery level is only updated when the sensor is triggered or through its daily heartbeat.
### Locks
-Read the instructions very carefully: sync with lock within 5 feet to avoid bad connection, link twice for both ON and OFF functionality.
+It is important to sync with the lock contorller within 5 feet to avoid bad connection and link twice for both ON and OFF functionality.
-**Items**
-Put something like this into your .items file:
+##### Items
```java
-Switch doorLock "Front Door [MAP(lock.map):%s]" { channel="insteon:device:home:AABBCC:switch" }
+Switch doorLock "Front Door [MAP(lock.map):%s]" { channel="insteon:device:home:aabbcc:lock" }
```
+
+ Legacy
+
+ ```java
+ Switch doorLock "Front Door [MAP(lock.map):%s]" { channel="insteon:device:home:AABBCC:switch" }
+ ```
+
+
+
and create a file "lock.map" in the transforms directory with these entries:
```text
-ON=Lock
-OFF=Unlock
+ON=locked
+OFF=unlocked
-=unknown
```
@@ -493,336 +1029,517 @@ This is based on the status of the contact when it is linked, and was intended f
The binding expects the contact to be inverted to work properly.
Ensure the contact is OFF (status LED is dark/garage door open) when linking the modem as a responder to the I/O Linc in order for it to function properly.
-Add this map into your transforms directory as "contact.map":
-
-```text
-OPEN=open
-CLOSED=closed
--=unknown
-```
-
-**Items**
-Along with this into your .items file:
+##### Items
```java
-Switch garageDoorOpener "garage door opener" { channel="insteon:device:home:AABBCC:switch", autoupdate="false" }
-Contact garageDoorContact "garage door contact [MAP(contact.map):%s]" { channel="insteon:device:home:AABBCC:contact" }
+Switch garageDoorOpener "door opener" { channel="insteon:device:home:aabbcc:switch" }
+Contact garageDoorContact "door contact [MAP(contact.map):%s]" { channel="insteon:device:home:aabbcc:contact" }
```
-**Sitemap**
-To make it visible in the GUI, put this into your sitemap file:
+and create a file "contact.map" in the transforms directory with these entries:
-```perl
-Switch item=garageDoorOpener label="garage door opener" mappings=[ ON="OPEN/CLOSE"]
-Text item=garageDoorContact
+```text
+OPEN=open
+CLOSED=closed
+-=unknown
```
-For safety reasons, only close the garage door if you have visual contact to make sure there is no obstruction! The use of automated rules for closing garage doors is dangerous.
-
> NOTE: If the I/O Linc contact status appears delayed, or returns the wrong value when the sensor changes states, the contact was likely ON (status LED lit) when the modem was linked as a responder.
Examples of this behavior would include: The status remaining CLOSED for up to 3 minutes after the door is opened, or the status remains OPEN for up to three minutes after the garage is opened and immediately closed again.
To resolve this behavior the I/O Linc will need to be unlinked and then re-linked to the modem with the contact OFF (stats LED off).
That would be with the door open when using the Insteon garage kit.
-### Keypads
+### Fan Controllers
-Before you attempt to configure the keypads, please familiarize yourself with the concept of an Insteon group.
+Here is an example configuration for a FanLinc module, which has a dimmable light and a variable speed fan:
-The Insteon keypad devices typically control one main load and have a number of buttons that will send out group broadcast messages to trigger a scene.
-If you just want to use the main load switch within openHAB just link modem and device with the set buttons as usual, no complicated linking is necessary.
-But if you want to get the buttons to work, read on.
+##### Items
-Each button will send out a message for a different, predefined group.
-Complicating matters further, the button numbering used internally by the device must be mapped to whatever labels are printed on the physical buttons of the device.
-Here is an example correspondence table:
+```java
+Dimmer fanLincDimmer "dimmer [%d %%]" { channel="insteon:device:home:aabbcc:dimmer" }
+String fanLincFan "fan speed" { channel="insteon:device:home:aabbcc:fan-speed" }
+```
-| Group | Button Number | 2487S Label |
-|-------|---------------|-------------|
-| 0x01 | 1 | (Load) |
-| 0x03 | 3 | A |
-| 0x04 | 4 | B |
-| 0x05 | 5 | C |
-| 0x06 | 6 | D |
+
+ Legacy
-When e.g. the "A" button is pressed (that's button #3 internally) a broadcast message will be sent out to all responders configured to listen to Insteon group #3.
-This means you must configure the modem as a responder to group #3 (and #4, #5, #6) messages coming from your keypad.
-For instructions how to do this, check out the [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal).
-You can even do that with the set buttons (see instructions that come with the keypad).
+ ```java
+ Dimmer fanLincDimmer "dimmer [%d %%]" { channel="insteon:device:home:AABBCC:lightDimmer" }
+ Number fanLincFan "fan" { channel="insteon:device:home:AABBCC:fan"}
+ ```
-While capturing the messages that the buttons emit is pretty straight forward, controlling the buttons is another matter.
-They cannot be simply toggled with a direct command to the device, but instead a broadcast message must be sent on a group number that the button has been programmed to listen to.
-This means you need to pick a set of unused groups that is globally unique (if you have multiple keypads, each one of them has to use different groups), one group for each button.
-The example configuration below uses groups 0xf3, 0xf4, 0xf5, and 0xf6.
-Then link the buttons such that they respond to those groups, and link the modem as a controller for them (see [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) documentation.
-In your items file you specify these groups with the "group=" parameters such that the binding knows what group number to put on the outgoing message.
+
-#### Keypad Switches
+##### Sitemap
-##### Items
+```perl
+Slider item=fanLincDimmer switchSupport
+Switch item=fanLincFan mappings=[ OFF="OFF", LOW="LOW", MEDIUM="MEDIUM", HIGH="HIGH" ]
+```
-Here is a simple example, just using the load (main) switch:
+### Power Meters
-```java
-Switch keypadSwitch "main load" { channel="insteon:device:home:AABBCC:loadSwitch" }
-Number keypadSwitchManualChange "main manual change" { channel="insteon:device:home:AABBCC:loadSwitchManualChange" }
-Switch keypadSwitchFastOnOff "main fast on/off" { channel="insteon:device:home:AABBCC:loadSwitchFastOnOff" }
-```
+The iMeter Solo reports both energy and power usage, and is updated during the normal polling process of the devices.
+Send a `REFRESH` command to force update the current values for the device.
+Additionally, the device can be reset.
-Most people will not use the fast on/off features or the manual change feature, so you really only need the first line.
-To make the buttons available, add the following:
+See the example below:
-###### Things
+##### Items
```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.15"] {
- Channels:
- Type keypadButtonA : keypadButtonA [ group="0xf3" ]
- Type keypadButtonB : keypadButtonB [ group="0xf4" ]
- Type keypadButtonC : keypadButtonC [ group="0xf5" ]
- Type keypadButtonD : keypadButtonD [ group="0xf6" ]
- }
-}
+Number:Power iMeterPower "power [%d W]" { channel="insteon:device:home:aabbcc:power-usage" }
+Number:Energy iMeterEnergy "energy [%.04f kWh]" { channel="insteon:device:home:aabbcc:energy-usage" }
+Switch iMeterReset "reset" { channel="insteon:device:home:aabbcc:reset" }
```
-The value after group must either be a number or string.
-The hexadecimal value 0xf3 can either converted to a numeric value 243 or the string value "0xf3".
+
+ Legacy
+
+ ```java
+ Number:Power iMeterWatts "iMeter [%d watts]" { channel="insteon:device:home:AABBCC:watts" }
+ Number:Energy iMeterKwh "iMeter [%.04f kWh]" { channel="insteon:device:home:AABBCC:kWh" }
+ Switch iMeterUpdate "iMeter Update" { channel="insteon:device:home:AABBCC:update" }
+ Switch iMeterReset "iMeter Reset" { channel="insteon:device:home:AABBCC:reset" }
+ ```
+
+
+
+### Sirens
+
+When turning on the siren directly, the binding will trigger the siren with no delay and up to the maximum duration (~2 minutes).
+The channels to change the alert delay and duration are only used for the siren arming behavior.
-###### Items
+Here is an example configuration for a siren module:
+
+##### Items
```java
-Switch keypadSwitchA "keypad button A" { channel="insteon:device:home:AABBCC:keypadButtonA"}
-Switch keypadSwitchB "keypad button B" { channel="insteon:device:home:AABBCC:keypadButtonB"}
-Switch keypadSwitchC "keypad button C" { channel="insteon:device:home:AABBCC:keypadButtonC"}
-Switch keypadSwitchD "keypad button D" { channel="insteon:device:home:AABBCC:keypadButtonD"}
+Switch siren "siren" { channel="insteon:device:home:aabbcc:siren" }
+Switch sirenArmed "armed" { channel="insteon:device:home:aabbcc:armed" }
+Switch sirenAlertDelay "alert delay" { channel="insteon:device:home:aabbcc:alert-delay" }
+Number:Time sirenAlertDuration "alert duration [%d s]" { channel="insteon:device:home:aabbcc:alert-duration" }
+String sirenAlertType "alert type [%s]" { channel="insteon:device:home:aabbcc:alert-type" }
```
##### Sitemap
-The following sitemap will bring the items to life in the GUI:
-
```perl
-Frame label="Keypad" {
- Switch item=keypadSwitch label="main"
- Switch item=keypadSwitchFastOnOff label="fast on/off"
- Switch item=keypadSwitchManualChange label="manual change" mappings=[ 0="DOWN", 1="STOP", 2="UP"]
- Switch item=keypadSwitchA label="button A"
- Switch item=keypadSwitchB label="button B"
- Switch item=keypadSwitchC label="button C"
- Switch item=keypadSwitchD label="button D"
-}
+Switch item=siren
+Text item=sirenArmed
+Switch item=sirenAlertDelay
+Setpoint item=sirenAlertDuration minValue=0 maxValue=127 step=1
+Switch item=sirenAlertType mappings=[ CHIME="CHIME", LOUD_SIREN="LOUD SIREN" ]
```
-#### Keypad Dimmers
+### Smoke Detectors
-The keypad dimmers are like keypad switches, except that the main load is dimmable.
+The smoke bridge monitors First Alert ONELINK smoke and carbon monoxide detectors.
+
+Here is an example configuration for a smoke bridge:
##### Items
```java
-Dimmer keypadDimmer "dimmer" { channel="insteon:device:home:AABBCC:loadDimmer" }
-Switch keypadDimmerButtonA "keypad dimmer button A [%d %%]" { channel="insteon:device:home:AABBCC:keypadButtonA" }
+Switch smokeAlarm "smoke alarm" { channel="insteon:device:home:aabbcc:smoke-alarm" }
+Switch carbonMonoxideAlarm "carbon monoxide alarm" { channel="insteon:device:home:aabbcc:carbon-monoxide-alarm" }
+Switch lowBattery "low battery" { channel="insteon:device:home:aabbcc:low-battery" }
```
-##### Sitemap
+### Sprinklers
-```perl
-Slider item=keypadDimmer switchSupport
-Switch item=keypadDimmerButtonA label="buttonA"
+The EZRain device controls up to 8 sprinkler valves and 4 programs.
+It can also enable pump control on the 8th valve.
+Only one sprinkler valve can be on at the time.
+When pump control is enabled, the 8th valve will remain on and cannot be controlled at the valve level.
+Each sprinkler program can be turned on/off by using `PLAY` and `PAUSE` commands.
+To skip forward or back to the next or previous valve in the program, use `NEXT` and `PREVIOUS` commands.
+
+##### Items
+
+```java
+Switch valve1 "valve 1" { channel="insteon:device:home:aabbcc:valve1" }
+Switch valve2 "valve 2" { channel="insteon:device:home:aabbcc:valve2" }
+Switch valve3 "valve 3" { channel="insteon:device:home:aabbcc:valve3" }
+Switch valve4 "valve 4" { channel="insteon:device:home:aabbcc:valve4" }
+Switch valve5 "valve 5" { channel="insteon:device:home:aabbcc:valve5" }
+Switch valve6 "valve 6" { channel="insteon:device:home:aabbcc:valve6" }
+Switch valve7 "valve 7" { channel="insteon:device:home:aabbcc:valve7" }
+Switch valve8 "valve 8" { channel="insteon:device:home:aabbcc:valve8" }
+Switch pump "pump" { channel="insteon:device:home:aabbcc:pump" }
+Player program1 "program 1" { channel="insteon:device:home:aabbcc:program1" }
+Player program2 "program 2" { channel="insteon:device:home:aabbcc:program2" }
+Player program3 "program 3" { channel="insteon:device:home:aabbcc:program3" }
+Player program4 "program 4" { channel="insteon:device:home:aabbcc:program4" }
```
### Thermostats
The thermostat (2441TH) is one of the most complex Insteon devices available.
-It must first be properly linked to the modem using configuration software like [Insteon Terminal](.
-The Insteon Terminal wiki describes in detail how to link the thermostat, and how to make it publish status update reports.
-
-When all is set and done the modem must be configured as a controller to group 0 (not sure why), and a responder to groups 1-5 such that it picks up when the thermostat switches on/off heating and cooling etc, and it must be a responder to special group 0xEF to get status update reports when measured values (temperature) change.
-Symmetrically, the thermostat must be a responder to group 0, and a controller for groups 1-5 and 0xEF.
-The linking process is not difficult but needs some persistence.
-Again, refer to the [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) documentation.
-
-#### Items
+To ensure all links are configured between the modem and device, and the status reporting is enabled, use the `insteon device addMissingLinks` console command.
-This is an example of what to put into your .items file:
+##### Items
```java
-Number thermostatCoolPoint "cool point [%.1f °F]" { channel="insteon:device:home:AABBCC:coolSetPoint" }
-Number thermostatHeatPoint "heat point [%.1f °F]" { channel="insteon:device:home:AABBCC:heatSetPoint" }
-Number thermostatSystemMode "system mode [%d]" { channel="insteon:device:home:AABBCC:systemMode" }
-Number thermostatFanMode "fan mode [%d]" { channel="insteon:device:home:AABBCC:fanMode" }
-Number thermostatIsHeating "is heating [%d]" { channel="insteon:device:home:AABBCC:isHeating"}
-Number thermostatIsCooling "is cooling [%d]" { channel="insteon:device:home:AABBCC:isCooling" }
-Number:Temperature thermostatTemperature "temperature [%.1f %unit%]" { channel="insteon:device:home:AABBCC:temperature" }
-Number thermostatHumidity "humidity [%.0f %%]" { channel="insteon:device:home:AABBCC:humidity" }
+Number:Temperature thermostatCoolSetpoint "cool setpoint [%.1f °F]" { channel="insteon:device:home:aabbcc:cool-setpoint" }
+Number:Temperature thermostatHeatSetpoint "heat setpoint [%.1f °F]" { channel="insteon:device:home:aabbcc:heat-setpoint" }
+String thermostatSystemMode "system mode [%s]" { channel="insteon:device:home:aabbcc:system-mode" }
+String thermostatSystemState "system state [%s]" { channel="insteon:device:home:aabbcc:system-state" }
+String thermostatFanMode "fan mode [%s]" { channel="insteon:device:home:aabbcc:fan-mode" }
+Number:Temperature thermostatTemperature "temperature [%.1f °F]" { channel="insteon:device:home:aabbcc:temperature" }
+Number:Dimensionless thermostatHumidity "humidity [%.0f %%]" { channel="insteon:device:home:aabbcc:humidity" }
```
Add this as well for some more exotic features:
```java
-Number thermostatACDelay "A/C delay [%d min]" { channel="insteon:device:home:AABBCC:acDelay" }
-Number thermostatBacklight "backlight [%d sec]" { channel="insteon:device:home:AABBCC:backlightDuration" }
-Number thermostatStage1 "A/C stage 1 time [%d min]" { channel="insteon:device:home:AABBCC:stage1Duration" }
-Number thermostatHumidityHigh "humidity high [%d %%]" { channel="insteon:device:home:AABBCC:humidityHigh" }
-Number thermostatHumidityLow "humidity low [%d %%]" { channel="insteon:device:home:AABBCC:humidityLow" }
+Number:Time thermostatACDelay "A/C delay [%d min]" { channel="insteon:device:home:aabbcc:ac-delay" }
+Number:Time thermostatBacklight "backlight [%d sec]" { channel="insteon:device:home:aabbcc:backlight-duration" }
+Number:Time thermostatStage1 "A/C stage 1 time [%d min]" { channel="insteon:device:home:aabbcc:stage1-duration" }
+Number:Dimensionless thermostatDehumidifySetpoint "dehumidify setpoint [%d %%]" { channel="insteon:device:home:aabbcc:dehumidify-setpoint" }
+Number:Dimensionless thermostatHumidifySetpoint "humidify setpoint [%d %%]" { channel="insteon:device:home:aabbcc:humidify-setpoint" }
+String thermostatTemperatureScale "temperature scale [%s]" { channel="insteon:device:home:aabbcc:temperature-scale" }
+String thermostatTimeFormat "time format [%s]" { channel="insteon:device:home:aabbcc:time=format" }
```
-#### Sitemap
+
+ Legacy
+
+ ```java
+ Number thermostatCoolPoint "cool point [%.1f °F]" { channel="insteon:device:home:AABBCC:coolSetPoint" }
+ Number thermostatHeatPoint "heat point [%.1f °F]" { channel="insteon:device:home:AABBCC:heatSetPoint" }
+ Number thermostatSystemMode "system mode [%d]" { channel="insteon:device:home:AABBCC:systemMode" }
+ Number thermostatFanMode "fan mode [%d]" { channel="insteon:device:home:AABBCC:fanMode" }
+ Number thermostatIsHeating "is heating [%d]" { channel="insteon:device:home:AABBCC:isHeating"}
+ Number thermostatIsCooling "is cooling [%d]" { channel="insteon:device:home:AABBCC:isCooling" }
+ Number:Temperature thermostatTemperature "temperature [%.1f %unit%]" { channel="insteon:device:home:AABBCC:temperature" }
+ Number thermostatHumidity "humidity [%.0f %%]" { channel="insteon:device:home:AABBCC:humidity" }
+ ```
+
+ Add this as well for some more exotic features:
+
+ ```java
+ Number thermostatACDelay "A/C delay [%d min]" { channel="insteon:device:home:AABBCC:acDelay" }
+ Number thermostatBacklight "backlight [%d sec]" { channel="insteon:device:home:AABBCC:backlightDuration" }
+ Number thermostatStage1 "A/C stage 1 time [%d min]" { channel="insteon:device:home:AABBCC:stage1Duration" }
+ Number thermostatHumidityHigh "humidity high [%d %%]" { channel="insteon:device:home:AABBCC:humidityHigh" }
+ Number thermostatHumidityLow "humidity low [%d %%]" { channel="insteon:device:home:AABBCC:humidityLow" }
+ ```
+
+
+
+##### Sitemap
For the thermostat to display in the GUI, add this to the sitemap file:
```perl
-Text item=thermostatTemperature icon="temperature"
-Text item=thermostatHumidity
+Text item=thermostatTemperature icon="temperature"
+Text item=thermostatHumidity
Setpoint item=thermostatCoolPoint icon="temperature" minValue=63 maxValue=90 step=1
Setpoint item=thermostatHeatPoint icon="temperature" minValue=50 maxValue=80 step=1
-Switch item=thermostatSystemMode label="system mode" mappings=[ 0="OFF", 1="HEAT", 2="COOL", 3="AUTO", 4="PROGRAM"]
-Switch item=thermostatFanMode label="fan mode" mappings=[ 0="AUTO", 1="ALWAYS ON"]
-Switch item=thermostatIsHeating label="is heating" mappings=[ 0="OFF", 1="HEATING"]
-Switch item=thermostatIsCooling label="is cooling" mappings=[ 0="OFF", 1="COOLING"]
-Setpoint item=thermostatACDelay minValue=2 maxValue=20 step=1
-Setpoint item=thermostatBacklight minValue=0 maxValue=100 step=1
-Setpoint item=thermostatHumidityHigh minValue=0 maxValue=100 step=1
-Setpoint item=thermostatHumidityLow minValue=0 maxValue=100 step=1
-Setpoint item=thermostatStage1 minValue=1 maxValue=60 step=1
+Switch item=thermostatSystemMode mappings=[ OFF="OFF", HEAT="HEAT", COOL="COOL", AUTO="AUTO", PROGRAM="PROGRAM" ]
+Text item=thermostatSystemState
+Switch item=thermostatFanMode mappings=[ AUTO="AUTO", ALWAYS_ON="ALWAYS ON" ]
+Setpoint item=thermostatACDelay minValue=2 maxValue=20 step=1
+Setpoint item=thermostatBacklight minValue=0 maxValue=100 step=1
+Setpoint item=thermostatDehumidifySetpoint minValue=20 maxValue=90 step=1
+Setpoint item=thermostatHumidifySetpoint minValue=0 maxValue=79 step=1
+Setpoint item=thermostatStage1 minValue=1 maxValue=60 step=1
+Switch item=thermostatTemperatureScale mappings=[ CELSIUS="CELSIUS", FAHRENHEIT="FAHRENHEIT" ]
```
-### Power Meters
+
+ Legacy
-The iMeter Solo reports both wattage and kilowatt hours, and is updated during the normal polling process of the devices.
-You can also manually update the current values from the device and reset the device.
-See the example below:
+ ```perl
+ Text item=thermostatTemperature icon="temperature"
+ Text item=thermostatHumidity
+ Setpoint item=thermostatCoolPoint icon="temperature" minValue=63 maxValue=90 step=1
+ Setpoint item=thermostatHeatPoint icon="temperature" minValue=50 maxValue=80 step=1
+ Switch item=thermostatSystemMode label="system mode" mappings=[ 0="OFF", 1="HEAT", 2="COOL", 3="AUTO", 4="PROGRAM"]
+ Switch item=thermostatFanMode label="fan mode" mappings=[ 0="AUTO", 1="ALWAYS ON"]
+ Switch item=thermostatIsHeating label="is heating" mappings=[ 0="OFF", 1="HEATING"]
+ Switch item=thermostatIsCooling label="is cooling" mappings=[ 0="OFF", 1="COOLING"]
+ Setpoint item=thermostatACDelay minValue=2 maxValue=20 step=1
+ Setpoint item=thermostatBacklight minValue=0 maxValue=100 step=1
+ Setpoint item=thermostatHumidityHigh minValue=0 maxValue=100 step=1
+ Setpoint item=thermostatHumidityLow minValue=0 maxValue=100 step=1
+ Setpoint item=thermostatStage1 minValue=1 maxValue=60 step=1
+ ```
+
+
-#### Items
+### Window Coverings
+
+Here is an example configuration for a micro open/close module (2444-222) in the .items file:
```java
-Number:Power iMeterWatts "iMeter [%d watts]" { channel="insteon:device:home:AABBCC:watts" }
-Number:Energy iMeterKwh "iMeter [%.04f kWh]" { channel="insteon:device:home:AABBCC:kWh" }
-Switch iMeterUpdate "iMeter Update" { channel="insteon:device:home:AABBCC:update" }
-Switch iMeterReset "iMeter Reset" { channel="insteon:device:home:AABBCC:reset" }
+Rollershutter windowShade "window shade" { channel="insteon:device:home:aabbcc:rollershutter" }
```
-### Fan Controllers
+Similar to [dimmers](#dimmers), the binding uses the device on level and ramp rate local settings to set the rollershutter level, the same way it would be set when physically interacting with the controller, and can be overridden using the `onLevel` and `rampRate`channel parameters.
-Here is an example configuration for a FanLinc module, which has a dimmable light and a variable speed fan:
+## Insteon Scenes
+
+The binding can trigger scenes by commanding the modem to send broadcasts to a given Insteon group.
-#### Items
+### Things
```java
-Dimmer fanLincDimmer "fanlinc dimmer [%d %%]" { channel="insteon:device:home:AABBCC:lightDimmer" }
-Number fanLincFan "fanlinc fan" { channel="insteon:device:home:AABBCC:fan"}
+Bridge insteon:plm:home [serialPort="/dev/ttyUSB0"] {
+ Thing scene scene42 [group=42]
+}
```
-#### Sitemap
+### Items
+
+```java
+Switch scene "scene" { channel="insteon:scene:home:scene42:scene" }
+Switch sceneFastOnOff "fast on/off" { channel="insteon:scene:home:scene42:fast-on-off" }
+Rollershutter sceneManualChange "manual change" { channel="insteon:scene:home:scene42:manual-change" }
+```
+
+### Sitemap
```perl
-Slider item=fanLincDimmer switchSupport
-Switch item=fanLincFan label="fan speed" mappings=[ 0="OFF", 1="LOW", 2="MEDIUM", 3="HIGH"]
+Switch item=scene
+Switch item=sceneFastOnOff mappings=[ ON="ON", OFF="OFF" ]
+Switch item=sceneManualChange mappings=[ UP="UP", DOWN="DOWN", STOP="STOP" ]
```
-### X10 Devices
+Sending `ON` command to `scene` will cause the modem to send a broadcast message to group 42, and all devices that are configured to respond to it should react.
+The current state of a scene is published on the `scene` channel.
+An `ON` state indicates that all the device states associated to a scene are matching their configured link on level.
-It is worth noting that both the Inseon PLM and the 2014 Hub can both command X10 devices over the powerline, and also set switch stats based on X10 signals received over the powerline.
-This allows openHAB not only control X10 devices without the need for other hardwaare, but it can also have rules that react to incoming X10 powerline commands.
-While you cannot bind the the X10 devices to the Insteon PLM/HUB, here are some examples for configuring X10 devices.
-Be aware that most X10 switches/dimmers send no status updates, i.e. openHAB will not learn about switches that are toggled manually.
-Further note that X10 devices are addressed with `houseCode.unitCode`, e.g. `A.2`.
+
+ Legacy
-#### Items
+ The binding can command the modem to send broadcasts to a given Insteon group.
+ Since it is a broadcast message, the corresponding item does _not_ take the address of any device, but of the modem itself.
+ The format is `broadcastOnOff#X` where X is the group that you want to be able to broadcast messages to:
-```java
-Switch x10Switch "X10 switch" { channel="insteon:device:home:AABB:switch" }
-Dimmer x10Dimmer "X10 dimmer" { channel="insteon:device:home:AABB:dimmer" }
-Contact x10Motion "X10 motion" { channel="insteon:device:home:AABB:contact" }
-```
+### Things
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="0x000045"] {
+ Channels:
+ Type broadcastOnOff : broadcastOnOff#2
+ }
+ }
+ ```
+
+ Or setting the device configuration parameter with a JSON object with `broadcastGroups` key and the broadcast group array value:
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="0x000045", deviceConfig="{'broadcastGroups': [2]}"]
+ }
+ ```
+
+### Items
+
+ ```java
+ Switch broadcastOnOff "group on/off" { channel="insteon:device:home:AABBCC:broadcastOnOff#2" }
+ ```
-## Direct Sending of Group Broadcasts (Triggering Scenes)
+ Flipping this switch to "ON" will cause the modem to send a broadcast message with group=2, and all devices that are configured to respond to it should react.
-The binding can command the modem to send broadcasts to a given Insteon group.
-Since it is a broadcast message, the corresponding item does _not_ take the address of any device, but of the modem itself.
-The format is `broadcastOnOff#X` where X is the group that you want to be able to broadcast messages to:
+
+
+## X10 Devices
+
+It is worth noting that both the Insteon PLM and the 2014 Hub can both command X10 devices over the powerline, and also set switch stats based on X10 signals received over the powerline.
+This allows openHAB not only control X10 devices without the need for other hardware, but it can also have rules that react to incoming X10 powerline commands.
+
+Note that X10 switches/dimmers send no status updates when toggled manually.
### Things
```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="0x000045"] {
- Channels:
- Type broadcastOnOff : broadcastOnOff#2
- }
+Bridge insteon:plm:home [serialPort="/dev/ttyUSB0"] {
+ Thing x10 a2 [houseCode="A", unitCode=2, deviceType="X10_Switch"]
+ Thing x10 b4 [houseCode="B", unitCode=4, deviceType="X10_Dimmer"]
+ Thing x10 c6 [houseCode="C", unitCode=6, deviceType="X10_Sensor"]
}
-
```
+
+ Legacy
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device A2 [address="A.2", productKey="X00.00.01"]
+ Thing device B4 [address="B.4", productKey="X00.00.02"]
+ Thing device C6 [address="C.6", productKey="X00.00.03"]
+ }
+ ```
+
+
+
### Items
```java
-Switch broadcastOnOff "group on/off" { channel="insteon:device:home:AABBCC:broadcastOnOff#2" }
+Switch x10Switch "X10 switch" { channel="insteon:x10:home:a2:switch" }
+Dimmer x10Dimmer "X10 dimmer" { channel="insteon:x10:home:b4:dimmer" }
+Contact x10Contact "X10 contact" { channel="insteon:x10:home:c6:contact" }
```
-Flipping this switch to "ON" will cause the modem to send a broadcast message with group=2, and all devices that are configured to respond to it should react.
-
-Channels can also be configured using the device configuration parameter of the device.
-The key in the JSON object is `broadcastGroups` and the value is an array of integers:
+## Battery Powered Devices
-### Things (device Config)
+Battery powered devices (mostly sensors) work differently than standard wired one.
+To conserve battery, these devices are only pollable when there are awake.
+Typically they send a heartbeat every 24 hours.
+When the binding receives a message from one of these devices, it polls additional information needed during the awake period (about 4 seconds).
+Some wireless devices have a `stay-awake` channel that can extend the period up to 4 minutes but at the cost of using more battery.
+It shouldn't be used in most cases except during initial device configuration.
+Same goes with commands, the binding will queue up commands requested on these devices and send them during the awake time window.
+Only one command per channel is queued, this mean that subsequent requests will overwrite previous ones.
-```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="0x000045", deviceConfig="{'broadcastGroups': [2]}"]
-}
+### Heartbeat Timeout Monitor
-```
+Sensor devices that supports heartbeat have a timeout monitor.
+If no broadcast message is received within a specific interval, the associated thing status will go offline until the binding receives a broadcast message from that device.
+The heartbeat interval on most sensor devices is hard coded as 24 hours but some have the ability to change that interval through the `heartbeat-interval` channel.
+It is enabled by default on devices that supports that feature and will be disabled on devices that have the ability to turn off their heartbeat through the `heartbeat-on-off` channel.
+It is important that the heartbeat group (typically 4) is linked properly to the modem by using the `insteon device addMissingLinks` console command.
+Otherwise, if the link is missing, the timeout monitor will be disabled.
+If necessary, the heartbeat timeout monitor can be manually reset by disabling and re-enabling the associated device thing.
-## Channel "related" Property
+## Related Devices
When an Insteon device changes its state because it is directly operated (for example by flipping a switch manually), it sends out a broadcast message to announce the state change, and the binding (if the PLM modem is properly linked as a responder) should update the corresponding openHAB items.
Other linked devices however may also change their state in response, but those devices will _not_ send out a broadcast message, and so openHAB will not learn about their state change until the next poll.
One common scenario is e.g. a switch in a 3-way configuration, with one switch controlling the load, and the other switch being linked as a controller.
-In this scenario, the "related" keyword can be used to have the binding poll a related device whenever a state change occurs for another device.
-A typical example would be two dimmers (A and B) in a 3-way configuration:
-
-```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [related="AA.BB.DD"]
+In this scenario, when the binding receives a broadcast message from one of these devices indicating a state change, it will poll the other related devices shortly after, instead of waiting until the next scheduled device poll which can take minutes.
+It is important to note, that the binding will now automatically determine related devices, based on device link databases, deprecating the `related` channel parameter.
+Likewise, the related devices from triggered button events will be polled as well.
+For scenes, these will be polled based on the modem database, after sending a group broadcast message.
+
+
+ Legacy
+
+ The `related` channel parameter can be used to have the binding poll a related device whenever a state change occurs for another device.
+ A typical example would be two dimmers (A and B) in a 3-way configuration:
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [related="AA.BB.DD"]
+ }
+ Thing device AABBDD [address="AA.BB.DD", productKey="F00.00.11"] {
+ Channels:
+ Type dimmer : dimmer [related="AA.BB.CC"]
+ }
}
- Thing device AABBDD [address="AA.BB.DD", productKey="F00.00.11"] {
- Channels:
- Type dimmer : dimmer [related="AA.BB.CC"]
+ ```
+
+ The binding doesn't know which devices have responded to the message since its a broadcast message.
+ The `related` channel parameter can be used to have the binding poll one or more related device when group message are sent.
+ More than one device can be polled by separating them with `+` sign.
+ A typical example would be a switch configured to broadcast to a group, and one or more devices configured to respond to the message:
+
+ ```java
+ Bridge insteon:network:home [port="/dev/ttyUSB0"] {
+ Thing device AABBCC [address="AA.BB.CC", productKey="0x000045"] {
+ Channels:
+ Type broadcastOnOff : broadcastOnOff#3 [related="AA.BB.DD+AA.BB.EE"]
+ }
+ Thing device AABBDD [address="AA.BB.DD", productKey="F00.00.11"]
+ Thing device AABBEE [address="AA.BB.EE", productKey="F00.00.11"]
}
-}
-```
+ ```
+
+
+
+## Triggered Events
-Another scenario is a group broadcast message, the binding doesn't know which devices have responded to the message since its a broadcast message.
-In this scenario, the "related" keyword can be used to have the binding poll one or more related device when group message are sent.
-A typical example would be a switch configured to broadcast to a group, and one or more devices configured to respond to the message:
+In order to monitor if an Insteon device button was directly operated and the type of interaction, triggered event channels can be used.
+These channels have the sole purpose to be used in rules in order to set off subsequent actions based on these events.
+Below are examples, including all available events, of a dimmer button and a keypad button:
```java
-Bridge insteon:network:home [port="/dev/ttyUSB0"] {
- Thing device AABBCC [address="AA.BB.CC", productKey="0x000045"] {
- Channels:
- Type broadcastOnOff : broadcastOnOff#3 [related="AA.BB.DD"]
+rule "Dimmer Paddle Events"
+when
+ Channel 'insteon:device:home:dimmer:event-button' triggered
+then
+ switch receivedEvent {
+ case PRESSED_ON: // do something (regular on)
+ case PRESSED_OFF: // do something (regular off)
+ case DOUBLE_PRESSED_ON: // do something (fast on)
+ case DOUBLE_PRESSED_OFF: // do something (fast off)
+ case HELD_UP: // do something (manual change up)
+ case HELD_DOWN: // do something (manual change down)
+ case RELEASED: // do something (manual change stop)
}
- Thing device AABBDD [address="AA.BB.DD", productKey="F00.00.11"]
-}
+end
+
+rule "Keypad Button A Pressed Off"
+when
+ Channel 'insteon:device:home:keypad:event-button-a' triggered PRESSED_OFF
+then
+ // do something
+end
```
-More than one device can be polled by separating them with "+" sign, e.g. "related=aa.bb.cc+xx.yy.zz" would poll both of these devices.
-The implemenation of the _related_ keyword is simple: if you add it to a channel, and that channel changes its state, then the _related_ device will be polled to see if its state has updated.
+## Migration Guide
+
+Here are the recommended steps to follow when migrating from the legacy implementation:
+
+- Create a new bridge matching your modem type.
+This will automatically disable the legacy network bridge with the same configuration to prevent having two bridges connected to the same modem.
+
+- Once your devices are discovered, they will show in your inbox.
+ - Add the discovered things.
+ - Connect the new things to your existing semantic models.
+ - Link the new channels to your existing items.
+ - Update your relevant rules.
+
+- For battery powered devices, press on their SET button to speed up the discovery process.
+Otherwise you may have to wait until the next time these devices send a heartbeat message which can take up to 24 hours.
+
+- For scenes, you can either enable scene discovery and add the discovered things, or just manually add specific scene things based on your existing environment.
+Enabling scene discovery might generate a considerable amount of things in your inbox depending on the number of scenes configured in your modem.
+
+- If you have rules to send commands to synchronize the state between related devices, you can enable the device synchronization feature on the bridge instead.
+This will synchronize related devices automatically based on their all-link database.
+
+- If you need to re-enable the legacy bridge, simply disable the new bridge and enable the legacy one again.
+
+- Once you finished updating your environment, you can remove the legacy bridge and things, which may need to be forced deleted since their bridge would be disabled.
## Troubleshooting
-Turn on DEBUG or TRACE logging for `org.openhab.binding.insteon.
+Turn on DEBUG or TRACE logging for `org.openhab.binding.insteon`.
See [logging in openHAB](https://www.openhab.org/docs/administration/logging.html) for more info.
+### Debug Console Commands
+
+To log message events between a device and the modem to a file:
+
+```shell
+# Single device monitor
+openhab> insteon debug startMonitoring AA.BB.CC
+# All devices monitor
+openhab> insteon debug startMonitoring --all
+```
+
+To send a message to a device or broadcast group:
+
+```shell
+# Standard message to a device
+openhab> insteon debug sendStandardMessage AA.BB.CC 11 FF
+# Broadcast message to a group
+openhab> insteon debug sendBroadcastMessage 42 13 00
+```
+
### Device Permissions / Linux Device Locks
When openHAB is running as a non-root user (Linux/OSX) it is important to ensure it has write access not just to the PLM device, but to the os lock directory.
Under openSUSE this is `/run/lock` and is managed by the **lock** group.
-Example commands to grant openHAB access (adjust for your distribution):
+Example commands to grant openHAB access, depending on Linux distribution:
```shell
usermod -a -G dialout openhab
@@ -831,66 +1548,64 @@ usermod -a -G lock openhab
Insufficient access to the lock directory will result in openHAB failing to access the device, even if the device itself is writable.
-### Adding New Device Types (Using Existing Device Features)
+## Legacy Device Customization
-Device types are defined in the file `device_types.xml`, which is inside the Insteon bundle and thus not visible to the user.
-You can however load your own device_types.xml by referencing it in the network config parameters:
+
-```ini
-additionalDevices="/usr/local/openhab/rt/my_own_devices.xml"
-```
+### Adding New Legacy Device Types (Using Existing Device Features)
+
+ Device types are defined in the file `legacy-device-types.xml`, which is inside the Insteon bundle and thus not visible to the user.
+ You can however load your own device_types.xml by referencing it in the network config parameters:
-Where the `my_own_devices.xml` file defines a new device like this:
+ ```ini
+ additionalDevices="/usr/local/openhab/rt/my-own-devices.xml"
+ ```
-```xml
-
-
+ Where the `my-own-devices.xml` file defines a new device like this:
+
+ ```xml
+
+
2456-D3
LampLinc V2
GenericDimmer
GenericLastTime
-
-
-```
-
-Finding the Insteon product key can be tricky since Insteon has not updated the product key table () since 2008.
-If a web search does not turn up the product key, make one up, starting with "F", like: F00.00.99.
-Avoid duplicate keys by finding the highest fake product key in the `device_types.xml` file, and incrementing by one.
-
-### Adding New Device Features
-
-If you can't build a new device out of the existing device features (for a complete list see `device_features.xml`) you can add new features by specifying a file (let's call it `my_own_features.xml`) with the "additionalDevices" option in the network config parameters:
-
-```ini
-additionalFeatures="/usr/local/openhab/rt/my_own_features.xml"
-```
-
-In this file you can define your own features (or even overwrite an existing feature.
-In the example below a new feature "MyFeature" is defined, which can then be referenced from the `device_types.xml` file (or from `my_own_devices.xml`):
-
-```xml
-
-
- DefaultDispatcher
- NoOpMsgHandler
- NoOpMsgHandler
- NoOpMsgHandler
- NoOpMsgHandler
- LightStateSwitchHandler
- IOLincOnOffCommandHandler
- DefaultPollHandler
-
-
-```
+
+
+ ```
+
+ Finding the Insteon product key can be tricky since Insteon has not updated the product key table () since 2008.
+ If a web search does not turn up the product key, make one up, starting with "F", like: F00.00.99.
+ Avoid duplicate keys by finding the highest fake product key in the `legacy-device-types.xml` file, and incrementing by one.
+
+### Adding New Legacy Device Features
+
+ If you can't build a new device out of the existing device features (for a complete list see `legacy-device-features.xml`) you can add new features by specifying a file (let's call it `my-own-features.xml`) with the "additionalDevices" option in the network config parameters:
+
+ ```ini
+ additionalFeatures="/usr/local/openhab/rt/my-own-features.xml"
+ ```
+
+ In this file you can define your own features (or even overwrite an existing feature).
+ In the example below a new feature "MyFeature" is defined, which can then be referenced from the `legacy-device-types.xml` file (or from `my-own-devices.xml`):
+
+ ```xml
+
+
+ DefaultDispatcher
+ NoOpMsgHandler
+ NoOpMsgHandler
+ NoOpMsgHandler
+ NoOpMsgHandler
+ LightStateSwitchHandler
+ IOLincOnOffCommandHandler
+ DefaultPollHandler
+
+
+ ```
+
+
## Known Limitations and Issues
-- Devices cannot be linked to the modem while the binding is running.
-If new devices are linked, the binding must be restarted.
-- Setting up Insteon groups and linking devices cannot be done from within openHAB.
-Use the [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) for that.
-If using Insteon Terminal (especially as root), ensure any stale lock files (For example, /var/lock/LCK..ttyUSB0) are removed before starting openHAB runtime.
-Failure to do so may result in "found no ports".
-- The Insteon PLM or hub is know to break in about 2-3 years due to poorly sized capacitors.
-You can repair it yourself using basic soldering skills, search for "Insteon PLM repair" or "Insteon hub repair".
-- Using the Insteon Hub 2014 in conjunction with other applications (such as the InsteonApp) is not supported. Concretely, openHAB will not learn when a switch is flipped via the Insteon App until the next poll, which could take minutes.
+- Using the Insteon binding in conjunction with other applications (such as the [Insteon Terminal](https://github.com/pfrommerd/insteon-terminal) or the Insteon App) can result in some unexpected behavior.
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java
index 10f431d7bce67..526bb8ddc3f0a 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBindingConstants.java
@@ -12,7 +12,13 @@
*/
package org.openhab.binding.insteon.internal;
+import java.io.File;
+import java.util.Map;
+import java.util.Set;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.VenstarSystemMode;
+import org.openhab.core.OpenHAB;
import org.openhab.core.thing.ThingTypeUID;
/**
@@ -20,93 +26,85 @@
* used across the whole binding.
*
* @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
*/
@NonNullByDefault
public class InsteonBindingConstants {
public static final String BINDING_ID = "insteon";
+ public static final String BINDING_DATA_DIR = OpenHAB.getUserDataFolder() + File.separator + BINDING_ID;
+
+ // List of all thing type uids
+ public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device");
+ public static final ThingTypeUID THING_TYPE_HUB1 = new ThingTypeUID(BINDING_ID, "hub1");
+ public static final ThingTypeUID THING_TYPE_HUB2 = new ThingTypeUID(BINDING_ID, "hub2");
+ public static final ThingTypeUID THING_TYPE_PLM = new ThingTypeUID(BINDING_ID, "plm");
+ public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene");
+ public static final ThingTypeUID THING_TYPE_X10 = new ThingTypeUID(BINDING_ID, "x10");
+ public static final ThingTypeUID THING_TYPE_LEGACY_DEVICE = new ThingTypeUID(BINDING_ID, "legacy-device");
+ public static final ThingTypeUID THING_TYPE_LEGACY_NETWORK = new ThingTypeUID(BINDING_ID, "network");
+
+ public static final Set DISCOVERABLE_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE, THING_TYPE_SCENE);
+ public static final Set DISCOVERABLE_LEGACY_THING_TYPES_UIDS = Set.of(THING_TYPE_LEGACY_DEVICE);
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE, THING_TYPE_HUB1,
+ THING_TYPE_HUB2, THING_TYPE_PLM, THING_TYPE_SCENE, THING_TYPE_X10, THING_TYPE_LEGACY_DEVICE,
+ THING_TYPE_LEGACY_NETWORK);
+
+ // List of all thing properties
+ public static final String PROPERTY_DEVICE_ADDRESS = "address";
+ public static final String PROPERTY_DEVICE_TYPE = "deviceType";
+ public static final String PROPERTY_ENGINE_VERSION = "engineVersion";
+ public static final String PROPERTY_PRODUCT_ID = "productId";
+ public static final String PROPERTY_SCENE_GROUP = "group";
+
+ // List of all channel parameters
+ public static final String PARAMETER_GROUP = "group";
+ public static final String PARAMETER_ON_LEVEL = "onLevel";
+ public static final String PARAMETER_RAMP_RATE = "rampRate";
+
+ // List of specific device feature names
+ public static final String FEATURE_DATABASE_DELTA = "databaseDelta";
+ public static final String FEATURE_HEARTBEAT = "heartbeat";
+ public static final String FEATURE_HEARTBEAT_INTERVAL = "heartbeatInterval";
+ public static final String FEATURE_HEARTBEAT_ON_OFF = "heartbeatOnOff";
+ public static final String FEATURE_INSTEON_ENGINE = "insteonEngine";
+ public static final String FEATURE_LED_CONTROL = "ledControl";
+ public static final String FEATURE_LED_ON_OFF = "ledOnOff";
+ public static final String FEATURE_LINK_FF_GROUP = "linkFFGroup";
+ public static final String FEATURE_LOW_BATTERY_THRESHOLD = "lowBatteryThreshold";
+ public static final String FEATURE_ON_LEVEL = "onLevel";
+ public static final String FEATURE_PING = "ping";
+ public static final String FEATURE_RAMP_RATE = "rampRate";
+ public static final String FEATURE_SCENE_ON_OFF = "sceneOnOff";
+ public static final String FEATURE_STAY_AWAKE = "stayAwake";
+ public static final String FEATURE_SYSTEM_MODE = "systemMode";
+ public static final String FEATURE_TEMPERATURE_SCALE = "temperatureScale";
+ public static final String FEATURE_TWO_GROUPS = "2Groups";
+
+ // List of specific device feature types
+ public static final String FEATURE_TYPE_FANLINC_FAN = "FanLincFan";
+ public static final String FEATURE_TYPE_GENERIC_DIMMER = "GenericDimmer";
+ public static final String FEATURE_TYPE_GENERIC_SWITCH = "GenericSwitch";
+ public static final String FEATURE_TYPE_KEYPAD_BUTTON = "KeypadButton";
+ public static final String FEATURE_TYPE_KEYPAD_BUTTON_OFF_MASK = "KeypadButtonOffMask";
+ public static final String FEATURE_TYPE_KEYPAD_BUTTON_ON_MASK = "KeypadButtonOnMask";
+ public static final String FEATURE_TYPE_KEYPAD_BUTTON_TOGGLE_MODE = "KeypadButtonToggleMode";
+ public static final String FEATURE_TYPE_OUTLET_SWITCH = "OutletSwitch";
+ public static final String FEATURE_TYPE_THERMOSTAT_FAN_MODE = "ThermostatFanMode";
+ public static final String FEATURE_TYPE_THERMOSTAT_SYSTEM_MODE = "ThermostatSystemMode";
+ public static final String FEATURE_TYPE_THERMOSTAT_COOL_SETPOINT = "ThermostatCoolSetpoint";
+ public static final String FEATURE_TYPE_THERMOSTAT_HEAT_SETPOINT = "ThermostatHeatSetpoint";
+ public static final String FEATURE_TYPE_VENSTAR_FAN_MODE = "VenstarFanMode";
+ public static final String FEATURE_TYPE_VENSTAR_SYSTEM_MODE = "VenstarSystemMode";
+ public static final String FEATURE_TYPE_VENSTAR_COOL_SETPOINT = "VenstarCoolSetpoint";
+ public static final String FEATURE_TYPE_VENSTAR_HEAT_SETPOINT = "VenstarHeatSetpoint";
- // List of all Thing Type UIDs
- public static final ThingTypeUID DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "device");
- public static final ThingTypeUID NETWORK_THING_TYPE = new ThingTypeUID(BINDING_ID, "network");
+ // List of specific device types
+ public static final String DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT = "ClimateControl_VenstarThermostat";
- // List of all Channel ids
- public static final String AC_DELAY = "acDelay";
- public static final String BACKLIGHT_DURATION = "backlightDuration";
- public static final String BATTERY_LEVEL = "batteryLevel";
- public static final String BATTERY_PERCENT = "batteryPercent";
- public static final String BATTERY_WATERMARK_LEVEL = "batteryWatermarkLevel";
- public static final String BEEP = "beep";
- public static final String BOTTOM_OUTLET = "bottomOutlet";
- public static final String BUTTON_A = "buttonA";
- public static final String BUTTON_B = "buttonB";
- public static final String BUTTON_C = "buttonC";
- public static final String BUTTON_D = "buttonD";
- public static final String BUTTON_E = "buttonE";
- public static final String BUTTON_F = "buttonF";
- public static final String BUTTON_G = "buttonG";
- public static final String BUTTON_H = "buttonH";
- public static final String BROADCAST_ON_OFF = "broadcastOnOff";
- public static final String CONTACT = "contact";
- public static final String COOL_SET_POINT = "coolSetPoint";
- public static final String DIMMER = "dimmer";
- public static final String FAN = "fan";
- public static final String FAN_MODE = "fanMode";
- public static final String FAST_ON_OFF = "fastOnOff";
- public static final String FAST_ON_OFF_BUTTON_A = "fastOnOffButtonA";
- public static final String FAST_ON_OFF_BUTTON_B = "fastOnOffButtonB";
- public static final String FAST_ON_OFF_BUTTON_C = "fastOnOffButtonC";
- public static final String FAST_ON_OFF_BUTTON_D = "fastOnOffButtonD";
- public static final String FAST_ON_OFF_BUTTON_E = "fastOnOffButtonE";
- public static final String FAST_ON_OFF_BUTTON_F = "fastOnOffButtonF";
- public static final String FAST_ON_OFF_BUTTON_G = "fastOnOffButtonG";
- public static final String FAST_ON_OFF_BUTTON_H = "fastOnOffButtonH";
- public static final String HEAT_SET_POINT = "heatSetPoint";
- public static final String HUMIDITY = "humidity";
- public static final String HUMIDITY_HIGH = "humidityHigh";
- public static final String HUMIDITY_LOW = "humidityLow";
- public static final String IS_COOLING = "isCooling";
- public static final String IS_HEATING = "isHeating";
- public static final String KEYPAD_BUTTON_A = "keypadButtonA";
- public static final String KEYPAD_BUTTON_B = "keypadButtonB";
- public static final String KEYPAD_BUTTON_C = "keypadButtonC";
- public static final String KEYPAD_BUTTON_D = "keypadButtonD";
- public static final String KEYPAD_BUTTON_E = "keypadButtonE";
- public static final String KEYPAD_BUTTON_F = "keypadButtonF";
- public static final String KEYPAD_BUTTON_G = "keypadButtonG";
- public static final String KEYPAD_BUTTON_H = "keypadButtonH";
- public static final String KWH = "kWh";
- public static final String LAST_HEARD_FROM = "lastHeardFrom";
- public static final String LED_BRIGHTNESS = "ledBrightness";
- public static final String LED_ONOFF = "ledOnOff";
- public static final String LIGHT_DIMMER = "lightDimmer";
- public static final String LIGHT_LEVEL = "lightLevel";
- public static final String LIGHT_LEVEL_ABOVE_THRESHOLD = "lightLevelAboveThreshold";
- public static final String LOAD_DIMMER = "loadDimmer";
- public static final String LOAD_SWITCH = "loadSwitch";
- public static final String LOAD_SWITCH_FAST_ON_OFF = "loadSwitchFastOnOff";
- public static final String LOAD_SWITCH_MANUAL_CHANGE = "loadSwitchManualChange";
- public static final String LOWBATTERY = "lowBattery";
- public static final String MANUAL_CHANGE = "manualChange";
- public static final String MANUAL_CHANGE_BUTTON_A = "manualChangeButtonA";
- public static final String MANUAL_CHANGE_BUTTON_B = "manualChangeButtonB";
- public static final String MANUAL_CHANGE_BUTTON_C = "manualChangeButtonC";
- public static final String MANUAL_CHANGE_BUTTON_D = "manualChangeButtonD";
- public static final String MANUAL_CHANGE_BUTTON_E = "manualChangeButtonE";
- public static final String MANUAL_CHANGE_BUTTON_F = "manualChangeButtonF";
- public static final String MANUAL_CHANGE_BUTTON_G = "manualChangeButtonG";
- public static final String MANUAL_CHANGE_BUTTON_H = "manualChangeButtonH";
- public static final String NOTIFICATION = "notification";
- public static final String ON_LEVEL = "onLevel";
- public static final String RAMP_DIMMER = "rampDimmer";
- public static final String RAMP_RATE = "rampRate";
- public static final String RESET = "reset";
- public static final String STAGE1_DURATION = "stage1Duration";
- public static final String SWITCH = "switch";
- public static final String SYSTEM_MODE = "systemMode";
- public static final String TAMPER_SWITCH = "tamperSwitch";
- public static final String TEMPERATURE = "temperature";
- public static final String TEMPERATURE_LEVEL = "temperatureLevel";
- public static final String TOP_OUTLET = "topOutlet";
- public static final String UPDATE = "update";
- public static final String WATTS = "watts";
+ // Map of custom state description options
+ public static final Map CUSTOM_STATE_DESCRIPTION_OPTIONS = Map.ofEntries(
+ // Venstar Thermostat System Mode
+ Map.entry(DEVICE_TYPE_CLIMATE_CONTROL_VENSTAR_THERMOSTAT + ":" + FEATURE_SYSTEM_MODE,
+ VenstarSystemMode.names().toArray(String[]::new)));
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonHandlerFactory.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonHandlerFactory.java
index d31796854323b..d130910550893 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonHandlerFactory.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonHandlerFactory.java
@@ -14,29 +14,34 @@
import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.insteon.internal.discovery.InsteonDeviceDiscoveryService;
+import org.openhab.binding.insteon.internal.discovery.InsteonDiscoveryService;
+import org.openhab.binding.insteon.internal.discovery.InsteonLegacyDiscoveryService;
+import org.openhab.binding.insteon.internal.handler.InsteonBridgeHandler;
import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
-import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonLegacyDeviceHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonLegacyNetworkHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonSceneHandler;
+import org.openhab.binding.insteon.internal.handler.X10DeviceHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingManager;
+import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -45,26 +50,29 @@
* handlers.
*
* @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
*/
@NonNullByDefault
@Component(configurationPid = "binding.insteon", service = ThingHandlerFactory.class)
public class InsteonHandlerFactory extends BaseThingHandlerFactory {
- private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
- .unmodifiableSet(Stream.of(DEVICE_THING_TYPE, NETWORK_THING_TYPE).collect(Collectors.toSet()));
-
+ private final SerialPortManager serialPortManager;
+ private final InsteonStateDescriptionProvider stateDescriptionProvider;
+ private final StorageService storageService;
+ private final ThingManager thingManager;
+ private final ThingRegistry thingRegistry;
private final Map> discoveryServiceRegs = new HashMap<>();
- private final Map> serviceRegs = new HashMap<>();
-
- private @Nullable SerialPortManager serialPortManager;
- @Reference
- protected void setSerialPortManager(final SerialPortManager serialPortManager) {
+ @Activate
+ public InsteonHandlerFactory(final @Reference SerialPortManager serialPortManager,
+ final @Reference InsteonStateDescriptionProvider stateDescriptionProvider,
+ final @Reference StorageService storageService, final @Reference ThingManager thingManager,
+ final @Reference ThingRegistry thingRegistry) {
this.serialPortManager = serialPortManager;
- }
-
- protected void unsetSerialPortManager(final SerialPortManager serialPortManager) {
- this.serialPortManager = null;
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ this.storageService = storageService;
+ this.thingManager = thingManager;
+ this.thingRegistry = thingRegistry;
}
@Override
@@ -76,40 +84,42 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
- if (NETWORK_THING_TYPE.equals(thingTypeUID)) {
- InsteonNetworkHandler insteonNetworkHandler = new InsteonNetworkHandler((Bridge) thing, serialPortManager);
- registerServices(insteonNetworkHandler);
-
- return insteonNetworkHandler;
- } else if (DEVICE_THING_TYPE.equals(thingTypeUID)) {
- return new InsteonDeviceHandler(thing);
+ if (THING_TYPE_HUB1.equals(thingTypeUID) || THING_TYPE_HUB2.equals(thingTypeUID)
+ || THING_TYPE_PLM.equals(thingTypeUID)) {
+ InsteonBridgeHandler handler = new InsteonBridgeHandler((Bridge) thing, serialPortManager, storageService,
+ thingRegistry);
+ InsteonDiscoveryService service = new InsteonDiscoveryService(handler);
+ registerDiscoveryService(handler, service);
+ return handler;
+ } else if (THING_TYPE_LEGACY_NETWORK.equals(thingTypeUID)) {
+ InsteonLegacyNetworkHandler handler = new InsteonLegacyNetworkHandler((Bridge) thing, serialPortManager,
+ thingManager, thingRegistry);
+ InsteonLegacyDiscoveryService service = new InsteonLegacyDiscoveryService(handler);
+ registerDiscoveryService(handler, service);
+ return handler;
+ } else if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
+ return new InsteonDeviceHandler(thing, stateDescriptionProvider);
+ } else if (THING_TYPE_LEGACY_DEVICE.equals(thingTypeUID)) {
+ return new InsteonLegacyDeviceHandler(thing);
+ } else if (THING_TYPE_SCENE.equals(thingTypeUID)) {
+ return new InsteonSceneHandler(thing);
+ } else if (THING_TYPE_X10.equals(thingTypeUID)) {
+ return new X10DeviceHandler(thing);
}
return null;
}
@Override
- protected synchronized void removeHandler(ThingHandler thingHandler) {
- if (thingHandler instanceof InsteonNetworkHandler) {
- ThingUID uid = thingHandler.getThing().getUID();
- ServiceRegistration> serviceRegs = this.serviceRegs.remove(uid);
- if (serviceRegs != null) {
- serviceRegs.unregister();
- }
-
- ServiceRegistration> discoveryServiceRegs = this.discoveryServiceRegs.remove(uid);
- if (discoveryServiceRegs != null) {
- discoveryServiceRegs.unregister();
- }
+ protected synchronized void removeHandler(ThingHandler handler) {
+ ServiceRegistration> serviceReg = discoveryServiceRegs.remove(handler.getThing().getUID());
+ if (serviceReg != null) {
+ serviceReg.unregister();
}
}
- private synchronized void registerServices(InsteonNetworkHandler handler) {
- this.serviceRegs.put(handler.getThing().getUID(),
- bundleContext.registerService(InsteonNetworkHandler.class.getName(), handler, new Hashtable<>()));
-
- InsteonDeviceDiscoveryService discoveryService = new InsteonDeviceDiscoveryService(handler);
- this.discoveryServiceRegs.put(handler.getThing().getUID(),
- bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
+ private synchronized void registerDiscoveryService(ThingHandler handler, DiscoveryService service) {
+ discoveryServiceRegs.put(handler.getThing().getUID(),
+ bundleContext.registerService(DiscoveryService.class.getName(), service, new Hashtable<>()));
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBinding.java
similarity index 50%
rename from bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java
rename to bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBinding.java
index 59b43d5413801..fc1ae270b7515 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonBinding.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBinding.java
@@ -12,7 +12,6 @@
*/
package org.openhab.binding.insteon.internal;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -26,38 +25,36 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
-import javax.xml.parsers.ParserConfigurationException;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
-import org.openhab.binding.insteon.internal.config.InsteonNetworkConfiguration;
-import org.openhab.binding.insteon.internal.device.DeviceFeature;
-import org.openhab.binding.insteon.internal.device.DeviceFeatureListener;
-import org.openhab.binding.insteon.internal.device.DeviceType;
-import org.openhab.binding.insteon.internal.device.DeviceTypeLoader;
+import org.openhab.binding.insteon.internal.config.InsteonLegacyChannelConfiguration;
+import org.openhab.binding.insteon.internal.config.InsteonLegacyNetworkConfiguration;
+import org.openhab.binding.insteon.internal.device.DeviceAddress;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
-import org.openhab.binding.insteon.internal.device.InsteonDevice;
-import org.openhab.binding.insteon.internal.device.InsteonDevice.DeviceStatus;
-import org.openhab.binding.insteon.internal.device.RequestQueueManager;
-import org.openhab.binding.insteon.internal.driver.Driver;
-import org.openhab.binding.insteon.internal.driver.DriverListener;
-import org.openhab.binding.insteon.internal.driver.ModemDBEntry;
-import org.openhab.binding.insteon.internal.driver.Poller;
-import org.openhab.binding.insteon.internal.driver.Port;
-import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
-import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
-import org.openhab.binding.insteon.internal.message.FieldException;
-import org.openhab.binding.insteon.internal.message.Msg;
-import org.openhab.binding.insteon.internal.message.MsgListener;
-import org.openhab.binding.insteon.internal.utils.Utils;
+import org.openhab.binding.insteon.internal.device.LegacyDevice;
+import org.openhab.binding.insteon.internal.device.LegacyDevice.DeviceStatus;
+import org.openhab.binding.insteon.internal.device.LegacyDeviceFeature;
+import org.openhab.binding.insteon.internal.device.LegacyDeviceType;
+import org.openhab.binding.insteon.internal.device.LegacyDeviceTypeLoader;
+import org.openhab.binding.insteon.internal.device.LegacyPollManager;
+import org.openhab.binding.insteon.internal.device.LegacyRequestManager;
+import org.openhab.binding.insteon.internal.device.X10Address;
+import org.openhab.binding.insteon.internal.device.database.LegacyModemDBEntry;
+import org.openhab.binding.insteon.internal.device.feature.LegacyFeatureListener;
+import org.openhab.binding.insteon.internal.device.feature.LegacyFeatureTemplateLoader;
+import org.openhab.binding.insteon.internal.handler.InsteonLegacyNetworkHandler;
+import org.openhab.binding.insteon.internal.transport.LegacyDriver;
+import org.openhab.binding.insteon.internal.transport.LegacyDriverListener;
+import org.openhab.binding.insteon.internal.transport.LegacyPort;
+import org.openhab.binding.insteon.internal.transport.LegacyPortListener;
+import org.openhab.binding.insteon.internal.transport.message.FieldException;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.xml.sax.SAXException;
/**
* A majority of the code in this file is from the openHAB 1 binding
@@ -104,34 +101,34 @@
* @author Bernd Pfrommer - Initial contribution
* @author Daniel Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
+ * @author Jeremy Setton - Rewrite insteon binding
*/
@NonNullByDefault
-public class InsteonBinding {
+public class InsteonLegacyBinding implements LegacyDriverListener, LegacyPortListener {
private static final int DEAD_DEVICE_COUNT = 10;
- private final Logger logger = LoggerFactory.getLogger(InsteonBinding.class);
+ private final Logger logger = LoggerFactory.getLogger(InsteonLegacyBinding.class);
- private Driver driver;
- private Map devices = new ConcurrentHashMap<>();
- private Map bindingConfigs = new ConcurrentHashMap<>();
- private PortListener portListener = new PortListener();
+ private LegacyDriver driver;
+ private Map devices = new ConcurrentHashMap<>();
+ private Map bindingConfigs = new ConcurrentHashMap<>();
private int devicePollIntervalMilliseconds = 300000;
private int deadDeviceTimeout = -1;
private boolean driverInitialized = false;
private int messagesReceived = 0;
private boolean isActive = false; // state of binding
private int x10HouseUnit = -1;
- private InsteonNetworkHandler handler;
+ private InsteonLegacyNetworkHandler handler;
- public InsteonBinding(InsteonNetworkHandler handler, InsteonNetworkConfiguration config,
+ public InsteonLegacyBinding(InsteonLegacyNetworkHandler handler, InsteonLegacyNetworkConfiguration config,
SerialPortManager serialPortManager, ScheduledExecutorService scheduler) {
this.handler = handler;
- String port = config.getPort();
- logger.debug("port = '{}'", Utils.redactPassword(port));
+ String port = config.getRedactedPort();
+ logger.debug("port = '{}'", port);
- driver = new Driver(port, portListener, serialPortManager, scheduler);
- driver.addMsgListener(portListener);
+ driver = new LegacyDriver(config, this, serialPortManager, scheduler);
+ driver.addPortListener(this);
Integer devicePollIntervalSeconds = config.getDevicePollIntervalSeconds();
if (devicePollIntervalSeconds != null) {
@@ -141,30 +138,21 @@ public InsteonBinding(InsteonNetworkHandler handler, InsteonNetworkConfiguration
String additionalDevices = config.getAdditionalDevices();
if (additionalDevices != null) {
- try {
- DeviceTypeLoader instance = DeviceTypeLoader.instance();
- if (instance != null) {
- instance.loadDeviceTypesXML(additionalDevices);
- logger.debug("read additional device definitions from {}", additionalDevices);
- } else {
- logger.warn("device type loader instance is null");
- }
- } catch (ParserConfigurationException | SAXException | IOException e) {
- logger.warn("error reading additional devices from {}", additionalDevices, e);
- }
+ logger.debug("loading additional device types from {}", additionalDevices);
+ LegacyDeviceTypeLoader.instance().loadDocument(additionalDevices);
}
String additionalFeatures = config.getAdditionalFeatures();
if (additionalFeatures != null) {
- logger.debug("reading additional feature templates from {}", additionalFeatures);
- DeviceFeature.readFeatureTemplates(additionalFeatures);
+ logger.debug("loading additional feature templates from {}", additionalFeatures);
+ LegacyFeatureTemplateLoader.instance().loadDocument(additionalFeatures);
}
deadDeviceTimeout = devicePollIntervalMilliseconds * DEAD_DEVICE_COUNT;
logger.debug("dead device timeout set to {} seconds", deadDeviceTimeout / 1000);
}
- public Driver getDriver() {
+ public LegacyDriver getDriver() {
return driver;
}
@@ -188,41 +176,41 @@ public void sendCommand(String channelName, Command command) {
return;
}
- InsteonChannelConfiguration bindingConfig = bindingConfigs.get(channelName);
+ InsteonLegacyChannelConfiguration bindingConfig = bindingConfigs.get(channelName);
if (bindingConfig == null) {
logger.warn("unable to find binding config for channel {}", channelName);
return;
}
- InsteonDevice dev = getDevice(bindingConfig.getAddress());
- if (dev == null) {
+ LegacyDevice device = getDevice(bindingConfig.getAddress());
+ if (device == null) {
logger.warn("no device found with insteon address {}", bindingConfig.getAddress());
return;
}
- dev.processCommand(driver, bindingConfig, command);
+ device.processCommand(driver, bindingConfig, command);
logger.debug("found binding config for channel {}", channelName);
}
- public void addFeatureListener(InsteonChannelConfiguration bindingConfig) {
+ public void addFeatureListener(InsteonLegacyChannelConfiguration bindingConfig) {
logger.debug("adding listener for channel {}", bindingConfig.getChannelName());
- InsteonAddress address = bindingConfig.getAddress();
- InsteonDevice dev = getDevice(address);
- if (dev == null) {
+ DeviceAddress address = bindingConfig.getAddress();
+ LegacyDevice device = getDevice(address);
+ if (device == null) {
logger.warn("device for address {} is null", address);
return;
}
@Nullable
- DeviceFeature f = dev.getFeature(bindingConfig.getFeature());
- if (f == null || f.isFeatureGroup()) {
+ LegacyDeviceFeature feature = device.getFeature(bindingConfig.getFeature());
+ if (feature == null || feature.isFeatureGroup()) {
StringBuilder buf = new StringBuilder();
- ArrayList names = new ArrayList<>(dev.getFeatures().keySet());
+ ArrayList names = new ArrayList<>(device.getFeatures().keySet());
Collections.sort(names);
for (String name : names) {
- DeviceFeature feature = dev.getFeature(name);
- if (feature != null && !feature.isFeatureGroup()) {
+ LegacyDeviceFeature f = device.getFeature(name);
+ if (f != null && !f.isFeatureGroup()) {
if (buf.length() > 0) {
buf.append(", ");
}
@@ -236,10 +224,10 @@ public void addFeatureListener(InsteonChannelConfiguration bindingConfig) {
return;
}
- DeviceFeatureListener fl = new DeviceFeatureListener(this, bindingConfig.getChannelUID(),
+ LegacyFeatureListener listener = new LegacyFeatureListener(this, bindingConfig.getChannelUID(),
bindingConfig.getChannelName());
- fl.setParameters(bindingConfig.getParameters());
- f.addListener(fl);
+ listener.setParameters(bindingConfig.getParameters());
+ feature.addListener(listener);
bindingConfigs.put(bindingConfig.getChannelName(), bindingConfig);
}
@@ -249,11 +237,11 @@ public void removeFeatureListener(ChannelUID channelUID) {
logger.debug("removing listener for channel {}", channelName);
- for (Iterator> it = devices.entrySet().iterator(); it.hasNext();) {
- InsteonDevice dev = it.next().getValue();
- boolean removedListener = dev.removeFeatureListener(channelName);
+ for (Iterator> it = devices.entrySet().iterator(); it.hasNext();) {
+ LegacyDevice device = it.next().getValue();
+ boolean removedListener = device.removeFeatureListener(channelName);
if (removedListener) {
- logger.trace("removed feature listener {} from dev {}", channelName, dev);
+ logger.trace("removed feature listener {} from device {}", channelName, device);
}
}
}
@@ -262,47 +250,43 @@ public void updateFeatureState(ChannelUID channelUID, State state) {
handler.updateState(channelUID, state);
}
- public @Nullable InsteonDevice makeNewDevice(InsteonAddress addr, String productKey,
+ public @Nullable LegacyDevice makeNewDevice(DeviceAddress address, String productKey,
Map deviceConfigMap) {
- DeviceTypeLoader instance = DeviceTypeLoader.instance();
- if (instance == null) {
- return null;
- }
- DeviceType dt = instance.getDeviceType(productKey);
- if (dt == null) {
+ LegacyDeviceType deviceType = LegacyDeviceTypeLoader.instance().getDeviceType(productKey);
+ if (deviceType == null) {
return null;
}
- InsteonDevice dev = InsteonDevice.makeDevice(dt);
- dev.setAddress(addr);
- dev.setProductKey(productKey);
- dev.setDriver(driver);
- dev.setIsModem(productKey.equals(InsteonDeviceHandler.PLM_PRODUCT_KEY));
- dev.setDeviceConfigMap(deviceConfigMap);
- if (!dev.hasValidPollingInterval()) {
- dev.setPollInterval(devicePollIntervalMilliseconds);
+ LegacyDevice device = LegacyDevice.makeDevice(deviceType);
+ device.setAddress(address);
+ device.setProductKey(productKey);
+ device.setDriver(driver);
+ device.setIsModem(productKey.equals(InsteonLegacyBindingConstants.PLM_PRODUCT_KEY));
+ device.setDeviceConfigMap(deviceConfigMap);
+ if (!device.hasValidPollingInterval()) {
+ device.setPollInterval(devicePollIntervalMilliseconds);
}
- if (driver.isModemDBComplete() && dev.getStatus() != DeviceStatus.POLLING) {
- int ndev = checkIfInModemDatabase(dev);
- if (dev.hasModemDBEntry()) {
- dev.setStatus(DeviceStatus.POLLING);
- Poller.instance().startPolling(dev, ndev);
+ if (driver.isModemDBComplete() && device.getStatus() != DeviceStatus.POLLING) {
+ int ndev = checkIfInModemDatabase(device);
+ if (device.hasModemDBEntry()) {
+ device.setStatus(DeviceStatus.POLLING);
+ LegacyPollManager.instance().startPolling(device, ndev);
}
}
- devices.put(addr, dev);
+ devices.put(address, device);
handler.insteonDeviceWasCreated();
- return (dev);
+ return device;
}
- public void removeDevice(InsteonAddress addr) {
- InsteonDevice dev = devices.remove(addr);
- if (dev == null) {
+ public void removeDevice(DeviceAddress address) {
+ LegacyDevice device = devices.remove(address);
+ if (device == null) {
return;
}
- if (dev.getStatus() == DeviceStatus.POLLING) {
- Poller.instance().stopPolling(dev);
+ if (device.getStatus() == DeviceStatus.POLLING) {
+ LegacyPollManager.instance().stopPolling(device);
}
}
@@ -310,22 +294,24 @@ public void removeDevice(InsteonAddress addr) {
* Checks if a device is in the modem link database, and, if the database
* is complete, logs a warning if the device is not present
*
- * @param dev The device to search for in the modem database
+ * @param device The device to search for in the modem database
* @return number of devices in modem database
*/
- private int checkIfInModemDatabase(InsteonDevice dev) {
+ private int checkIfInModemDatabase(LegacyDevice device) {
try {
- InsteonAddress addr = dev.getAddress();
- Map dbes = driver.lockModemDBEntries();
- if (dbes.containsKey(addr)) {
- if (!dev.hasModemDBEntry()) {
- logger.debug("device {} found in the modem database and {}.", addr, getLinkInfo(dbes, addr, true));
- dev.setHasModemDBEntry(true);
- }
- } else {
- if (driver.isModemDBComplete() && !addr.isX10()) {
- logger.warn("device {} not found in the modem database. Did you forget to link?", addr);
- handler.deviceNotLinked(addr);
+ Map dbes = driver.lockModemDBEntries();
+ if (device.getAddress() instanceof InsteonAddress address) {
+ if (dbes.containsKey(address)) {
+ if (!device.hasModemDBEntry()) {
+ logger.debug("device {} found in the modem database and {}.", address,
+ getLinkInfo(dbes, address, true));
+ device.setHasModemDBEntry(true);
+ }
+ } else {
+ if (driver.isModemDBComplete()) {
+ logger.warn("device {} not found in the modem database. Did you forget to link?", address);
+ handler.deviceNotLinked(address);
+ }
}
}
return dbes.size();
@@ -337,10 +323,9 @@ private int checkIfInModemDatabase(InsteonDevice dev) {
public Map getDatabaseInfo() {
try {
Map databaseInfo = new HashMap<>();
- Map dbes = driver.lockModemDBEntries();
- for (InsteonAddress addr : dbes.keySet()) {
- String a = addr.toString();
- databaseInfo.put(a, a + ": " + getLinkInfo(dbes, addr, false));
+ Map dbes = driver.lockModemDBEntries();
+ for (InsteonAddress address : dbes.keySet()) {
+ databaseInfo.put(address.toString(), address + ": " + getLinkInfo(dbes, address, false));
}
return databaseInfo;
@@ -365,50 +350,49 @@ public void shutdown() {
logger.debug("shutting down Insteon bridge");
driver.stop();
devices.clear();
- RequestQueueManager.destroyInstance();
- Poller.instance().stop();
+ LegacyRequestManager.destroyInstance();
+ LegacyPollManager.instance().stop();
isActive = false;
}
/**
* Method to find a device by address
*
- * @param aAddr the insteon address to search for
+ * @param address the insteon address to search for
* @return reference to the device, or null if not found
*/
- public @Nullable InsteonDevice getDevice(@Nullable InsteonAddress aAddr) {
- InsteonDevice dev = (aAddr == null) ? null : devices.get(aAddr);
- return (dev);
+ public @Nullable LegacyDevice getDevice(@Nullable DeviceAddress address) {
+ return address == null ? null : devices.get(address);
}
- private String getLinkInfo(Map dbes, InsteonAddress a, boolean prefix) {
- ModemDBEntry dbe = dbes.get(a);
+ private String getLinkInfo(Map dbes, InsteonAddress address, boolean prefix) {
+ LegacyModemDBEntry dbe = dbes.get(address);
if (dbe == null) {
return "";
}
List controls = dbe.getControls();
List responds = dbe.getRespondsTo();
- Port port = dbe.getPort();
+ LegacyPort port = dbe.getPort();
if (port == null) {
return "";
}
- String deviceName = port.getDeviceName();
- String s = deviceName.startsWith("/hub") ? "hub" : "plm";
+ String portName = port.getName();
+ String modemType = portName.startsWith("/hub") ? "hub" : "plm";
StringBuilder buf = new StringBuilder();
- if (port.isModem(a)) {
+ if (port.isModem(address)) {
if (prefix) {
buf.append("it is the ");
}
- buf.append(s);
+ buf.append(modemType);
buf.append(" (");
- buf.append(Utils.redactPassword(deviceName));
+ buf.append(portName);
buf.append(")");
} else {
if (prefix) {
buf.append("the ");
}
- buf.append(s);
+ buf.append(modemType);
buf.append(" controls groups (");
buf.append(toGroupString(controls));
buf.append(") and responds to groups (");
@@ -443,136 +427,124 @@ public int compare(Byte b1, Byte b2) {
public void logDeviceStatistics() {
String msg = String.format("devices: %3d configured, %3d polling, msgs received: %5d", devices.size(),
- Poller.instance().getSizeOfQueue(), messagesReceived);
+ LegacyPollManager.instance().getSizeOfQueue(), messagesReceived);
logger.debug("{}", msg);
messagesReceived = 0;
- for (InsteonDevice dev : devices.values()) {
- if (dev.isModem()) {
+ for (LegacyDevice device : devices.values()) {
+ if (device.isModem()) {
continue;
}
- if (deadDeviceTimeout > 0 && dev.getPollOverDueTime() > deadDeviceTimeout) {
- logger.debug("device {} has not responded to polls for {} sec", dev.toString(),
- dev.getPollOverDueTime() / 3600);
+ if (deadDeviceTimeout > 0 && device.getPollOverDueTime() > deadDeviceTimeout) {
+ logger.debug("device {} has not responded to polls for {} sec", device.toString(),
+ device.getPollOverDueTime() / 3600);
}
}
}
- /**
- * Handles messages that come in from the ports.
- * Will only process one message at a time.
- */
- private class PortListener implements MsgListener, DriverListener {
- @Override
- public void msg(Msg msg) {
- if (msg.isEcho() || msg.isPureNack()) {
- return;
- }
- messagesReceived++;
- logger.debug("got msg: {}", msg);
+ @Override
+ public void msg(Msg msg) {
+ if (msg.isEcho() || msg.isPureNack()) {
+ return;
+ }
+ messagesReceived++;
+ logger.debug("got msg: {}", msg);
+ try {
if (msg.isX10()) {
handleX10Message(msg);
- } else {
+ } else if (msg.isInsteon()) {
handleInsteonMessage(msg);
}
+ } catch (FieldException e) {
+ logger.warn("got bad message: {}", msg, e);
}
+ }
- @Override
- public void driverCompletelyInitialized() {
- List missing = new ArrayList<>();
- try {
- Map dbes = driver.lockModemDBEntries();
- logger.debug("modem database has {} entries!", dbes.size());
- if (dbes.isEmpty()) {
- logger.warn("the modem link database is empty!");
- }
- for (InsteonAddress k : dbes.keySet()) {
- logger.debug("modem db entry: {}", k);
- }
- Set addrs = new HashSet<>();
- for (InsteonDevice dev : devices.values()) {
- InsteonAddress a = dev.getAddress();
- if (!dbes.containsKey(a)) {
- if (!a.isX10()) {
- logger.warn("device {} not found in the modem database. Did you forget to link?", a);
- handler.deviceNotLinked(a);
- }
+ @Override
+ public void driverCompletelyInitialized() {
+ List missing = new ArrayList<>();
+ try {
+ Map dbes = driver.lockModemDBEntries();
+ logger.debug("modem database has {} entries!", dbes.size());
+ if (dbes.isEmpty()) {
+ logger.warn("the modem link database is empty!");
+ }
+ for (InsteonAddress address : dbes.keySet()) {
+ logger.debug("modem db entry: {}", address);
+ }
+ Set addrs = new HashSet<>();
+ for (LegacyDevice device : devices.values()) {
+ if (device.getAddress() instanceof InsteonAddress address) {
+ if (!dbes.containsKey(address)) {
+ logger.warn("device {} not found in the modem database. Did you forget to link?", address);
+ handler.deviceNotLinked(address);
} else {
- if (!dev.hasModemDBEntry()) {
- addrs.add(a);
- logger.debug("device {} found in the modem database and {}.", a,
- getLinkInfo(dbes, a, true));
- dev.setHasModemDBEntry(true);
+ if (!device.hasModemDBEntry()) {
+ addrs.add(address);
+ logger.debug("device {} found in the modem database and {}.", address,
+ getLinkInfo(dbes, address, true));
+ device.setHasModemDBEntry(true);
}
- if (dev.getStatus() != DeviceStatus.POLLING) {
- Poller.instance().startPolling(dev, dbes.size());
+ if (device.getStatus() != DeviceStatus.POLLING) {
+ LegacyPollManager.instance().startPolling(device, dbes.size());
}
}
}
+ }
- for (InsteonAddress k : dbes.keySet()) {
- if (!addrs.contains(k)) {
- logger.debug("device {} found in the modem database, but is not configured as a thing and {}.",
- k, getLinkInfo(dbes, k, true));
+ for (InsteonAddress address : dbes.keySet()) {
+ if (!addrs.contains(address)) {
+ logger.debug("device {} found in the modem database, but is not configured as a thing and {}.",
+ address, getLinkInfo(dbes, address, true));
- missing.add(k.toString());
- }
+ missing.add(address);
}
- } finally {
- driver.unlockModemDBEntries();
}
-
- if (!missing.isEmpty()) {
- handler.addMissingDevices(missing);
- }
-
- driverInitialized = true;
+ } finally {
+ driver.unlockModemDBEntries();
}
- @Override
- public void disconnected() {
- handler.bindingDisconnected();
+ if (!missing.isEmpty()) {
+ handler.addMissingDevices(missing);
}
- private void handleInsteonMessage(Msg msg) {
- InsteonAddress toAddr = msg.getAddr("toAddress");
- if (!msg.isBroadcast() && !driver.isMsgForUs(toAddr)) {
- // not for one of our modems, do not process
- return;
- }
- InsteonAddress fromAddr = msg.getAddr("fromAddress");
- if (fromAddr == null) {
- logger.debug("invalid fromAddress, ignoring msg {}", msg);
- return;
- }
- handleMessage(fromAddr, msg);
+ driverInitialized = true;
+ }
+
+ @Override
+ public void disconnected() {
+ handler.bindingDisconnected();
+ }
+
+ private void handleInsteonMessage(Msg msg) throws FieldException {
+ InsteonAddress toAddr = msg.getInsteonAddress("toAddress");
+ if (!msg.isBroadcast() && !driver.isMsgForUs(toAddr)) {
+ // not for one of our modems, do not process
+ return;
}
+ InsteonAddress fromAddr = msg.getInsteonAddress("fromAddress");
+ handleMessage(fromAddr, msg);
+ }
- private void handleX10Message(Msg msg) {
- try {
- int x10Flag = msg.getByte("X10Flag") & 0xff;
- int rawX10 = msg.getByte("rawX10") & 0xff;
- if (x10Flag == 0x80) { // actual command
- if (x10HouseUnit != -1) {
- InsteonAddress fromAddr = new InsteonAddress((byte) x10HouseUnit);
- handleMessage(fromAddr, msg);
- }
- } else if (x10Flag == 0) {
- // what unit the next cmd will apply to
- x10HouseUnit = rawX10 & 0xFF;
- }
- } catch (FieldException e) {
- logger.warn("got bad X10 message: {}", msg, e);
- return;
+ private void handleX10Message(Msg msg) throws FieldException {
+ int x10Flag = msg.getByte("X10Flag") & 0xff;
+ int rawX10 = msg.getByte("rawX10") & 0xff;
+ if (x10Flag == 0x80) { // actual command
+ if (x10HouseUnit != -1) {
+ X10Address fromAddr = new X10Address((byte) x10HouseUnit);
+ handleMessage(fromAddr, msg);
}
+ } else if (x10Flag == 0) {
+ // what unit the next cmd will apply to
+ x10HouseUnit = rawX10 & 0xFF;
}
+ }
- private void handleMessage(InsteonAddress fromAddr, Msg msg) {
- InsteonDevice dev = getDevice(fromAddr);
- if (dev == null) {
- logger.debug("dropping message from unknown device with address {}", fromAddr);
- } else {
- dev.handleMessage(msg);
- }
+ private void handleMessage(DeviceAddress fromAddr, Msg msg) {
+ LegacyDevice device = getDevice(fromAddr);
+ if (device == null) {
+ logger.debug("dropping message from unknown device with address {}", fromAddr);
+ } else {
+ device.handleMessage(msg);
}
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBindingConstants.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBindingConstants.java
new file mode 100644
index 0000000000000..fc21dc1b0ec55
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonLegacyBindingConstants.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link InsteonLegacyBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
+ */
+@NonNullByDefault
+public class InsteonLegacyBindingConstants {
+ // List of all Channel ids
+ public static final String AC_DELAY = "acDelay";
+ public static final String BACKLIGHT_DURATION = "backlightDuration";
+ public static final String BATTERY_LEVEL = "batteryLevel";
+ public static final String BATTERY_PERCENT = "batteryPercent";
+ public static final String BATTERY_WATERMARK_LEVEL = "batteryWatermarkLevel";
+ public static final String BEEP = "beep";
+ public static final String BOTTOM_OUTLET = "bottomOutlet";
+ public static final String BUTTON_A = "buttonA";
+ public static final String BUTTON_B = "buttonB";
+ public static final String BUTTON_C = "buttonC";
+ public static final String BUTTON_D = "buttonD";
+ public static final String BUTTON_E = "buttonE";
+ public static final String BUTTON_F = "buttonF";
+ public static final String BUTTON_G = "buttonG";
+ public static final String BUTTON_H = "buttonH";
+ public static final String BROADCAST_ON_OFF = "broadcastOnOff";
+ public static final String CONTACT = "contact";
+ public static final String COOL_SET_POINT = "coolSetPoint";
+ public static final String DIMMER = "dimmer";
+ public static final String FAN = "fan";
+ public static final String FAN_MODE = "fanMode";
+ public static final String FAST_ON_OFF = "fastOnOff";
+ public static final String FAST_ON_OFF_BUTTON_A = "fastOnOffButtonA";
+ public static final String FAST_ON_OFF_BUTTON_B = "fastOnOffButtonB";
+ public static final String FAST_ON_OFF_BUTTON_C = "fastOnOffButtonC";
+ public static final String FAST_ON_OFF_BUTTON_D = "fastOnOffButtonD";
+ public static final String FAST_ON_OFF_BUTTON_E = "fastOnOffButtonE";
+ public static final String FAST_ON_OFF_BUTTON_F = "fastOnOffButtonF";
+ public static final String FAST_ON_OFF_BUTTON_G = "fastOnOffButtonG";
+ public static final String FAST_ON_OFF_BUTTON_H = "fastOnOffButtonH";
+ public static final String HEAT_SET_POINT = "heatSetPoint";
+ public static final String HUMIDITY = "humidity";
+ public static final String HUMIDITY_HIGH = "humidityHigh";
+ public static final String HUMIDITY_LOW = "humidityLow";
+ public static final String IS_COOLING = "isCooling";
+ public static final String IS_HEATING = "isHeating";
+ public static final String KEYPAD_BUTTON_A = "keypadButtonA";
+ public static final String KEYPAD_BUTTON_B = "keypadButtonB";
+ public static final String KEYPAD_BUTTON_C = "keypadButtonC";
+ public static final String KEYPAD_BUTTON_D = "keypadButtonD";
+ public static final String KEYPAD_BUTTON_E = "keypadButtonE";
+ public static final String KEYPAD_BUTTON_F = "keypadButtonF";
+ public static final String KEYPAD_BUTTON_G = "keypadButtonG";
+ public static final String KEYPAD_BUTTON_H = "keypadButtonH";
+ public static final String KWH = "kWh";
+ public static final String LAST_HEARD_FROM = "lastHeardFrom";
+ public static final String LED_BRIGHTNESS = "ledBrightness";
+ public static final String LED_ONOFF = "ledOnOff";
+ public static final String LIGHT_DIMMER = "lightDimmer";
+ public static final String LIGHT_LEVEL = "lightLevel";
+ public static final String LIGHT_LEVEL_ABOVE_THRESHOLD = "lightLevelAboveThreshold";
+ public static final String LOAD_DIMMER = "loadDimmer";
+ public static final String LOAD_SWITCH = "loadSwitch";
+ public static final String LOAD_SWITCH_FAST_ON_OFF = "loadSwitchFastOnOff";
+ public static final String LOAD_SWITCH_MANUAL_CHANGE = "loadSwitchManualChange";
+ public static final String LOWBATTERY = "lowBattery";
+ public static final String MANUAL_CHANGE = "manualChange";
+ public static final String MANUAL_CHANGE_BUTTON_A = "manualChangeButtonA";
+ public static final String MANUAL_CHANGE_BUTTON_B = "manualChangeButtonB";
+ public static final String MANUAL_CHANGE_BUTTON_C = "manualChangeButtonC";
+ public static final String MANUAL_CHANGE_BUTTON_D = "manualChangeButtonD";
+ public static final String MANUAL_CHANGE_BUTTON_E = "manualChangeButtonE";
+ public static final String MANUAL_CHANGE_BUTTON_F = "manualChangeButtonF";
+ public static final String MANUAL_CHANGE_BUTTON_G = "manualChangeButtonG";
+ public static final String MANUAL_CHANGE_BUTTON_H = "manualChangeButtonH";
+ public static final String NOTIFICATION = "notification";
+ public static final String ON_LEVEL = "onLevel";
+ public static final String RAMP_DIMMER = "rampDimmer";
+ public static final String RAMP_RATE = "rampRate";
+ public static final String RESET = "reset";
+ public static final String STAGE1_DURATION = "stage1Duration";
+ public static final String SWITCH = "switch";
+ public static final String SYSTEM_MODE = "systemMode";
+ public static final String TAMPER_SWITCH = "tamperSwitch";
+ public static final String TEMPERATURE = "temperature";
+ public static final String TEMPERATURE_LEVEL = "temperatureLevel";
+ public static final String TOP_OUTLET = "topOutlet";
+ public static final String UPDATE = "update";
+ public static final String WATTS = "watts";
+
+ public static final Set ALL_CHANNEL_IDS = Set.of(AC_DELAY, BACKLIGHT_DURATION, BATTERY_LEVEL,
+ BATTERY_PERCENT, BATTERY_WATERMARK_LEVEL, BEEP, BOTTOM_OUTLET, BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_D,
+ BUTTON_E, BUTTON_F, BUTTON_G, BUTTON_H, BROADCAST_ON_OFF, CONTACT, COOL_SET_POINT, DIMMER, FAN, FAN_MODE,
+ FAST_ON_OFF, FAST_ON_OFF_BUTTON_A, FAST_ON_OFF_BUTTON_B, FAST_ON_OFF_BUTTON_C, FAST_ON_OFF_BUTTON_D,
+ FAST_ON_OFF_BUTTON_E, FAST_ON_OFF_BUTTON_F, FAST_ON_OFF_BUTTON_G, FAST_ON_OFF_BUTTON_H, HEAT_SET_POINT,
+ HUMIDITY, HUMIDITY_HIGH, HUMIDITY_LOW, IS_COOLING, IS_HEATING, KEYPAD_BUTTON_A, KEYPAD_BUTTON_B,
+ KEYPAD_BUTTON_C, KEYPAD_BUTTON_D, KEYPAD_BUTTON_E, KEYPAD_BUTTON_F, KEYPAD_BUTTON_G, KEYPAD_BUTTON_H, KWH,
+ LAST_HEARD_FROM, LED_BRIGHTNESS, LED_ONOFF, LIGHT_DIMMER, LIGHT_LEVEL, LIGHT_LEVEL_ABOVE_THRESHOLD,
+ LOAD_DIMMER, LOAD_SWITCH, LOAD_SWITCH_FAST_ON_OFF, LOAD_SWITCH_MANUAL_CHANGE, LOWBATTERY, MANUAL_CHANGE,
+ MANUAL_CHANGE_BUTTON_A, MANUAL_CHANGE_BUTTON_B, MANUAL_CHANGE_BUTTON_C, MANUAL_CHANGE_BUTTON_D,
+ MANUAL_CHANGE_BUTTON_E, MANUAL_CHANGE_BUTTON_F, MANUAL_CHANGE_BUTTON_G, MANUAL_CHANGE_BUTTON_H,
+ NOTIFICATION, ON_LEVEL, RAMP_DIMMER, RAMP_RATE, RESET, STAGE1_DURATION, SWITCH, SYSTEM_MODE, TAMPER_SWITCH,
+ TEMPERATURE, TEMPERATURE_LEVEL, TOP_OUTLET, UPDATE, WATTS);
+
+ public static final String BROADCAST_GROUPS = "broadcastGroups";
+ public static final String CMD = "cmd";
+ public static final String CMD_RESET = "reset";
+ public static final String CMD_UPDATE = "update";
+ public static final String DATA = "data";
+ public static final String FIELD = "field";
+ public static final String FIELD_BATTERY_LEVEL = "battery_level";
+ public static final String FIELD_BATTERY_PERCENTAGE = "battery_percentage";
+ public static final String FIELD_BATTERY_WATERMARK_LEVEL = "battery_watermark_level";
+ public static final String FIELD_KWH = "kwh";
+ public static final String FIELD_LIGHT_LEVEL = "light_level";
+ public static final String FIELD_TEMPERATURE_LEVEL = "temperature_level";
+ public static final String FIELD_WATTS = "watts";
+ public static final String GROUP = "group";
+ public static final String METER = "meter";
+
+ public static final String HIDDEN_DOOR_SENSOR_PRODUCT_KEY = "F00.00.03";
+ public static final String MOTION_SENSOR_II_PRODUCT_KEY = "F00.00.24";
+ public static final String MOTION_SENSOR_PRODUCT_KEY = "0x00004A";
+ public static final String PLM_PRODUCT_KEY = "0x000045";
+ public static final String POWER_METER_PRODUCT_KEY = "F00.00.17";
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonResourceLoader.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonResourceLoader.java
new file mode 100644
index 0000000000000..78a9a8eb9f322
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonResourceLoader.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+/**
+ * The {@link InsteonResourceLoader} represents an abstract Insteon resource loader
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public abstract class InsteonResourceLoader {
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final String name;
+
+ protected InsteonResourceLoader(String name) {
+ this.name = name;
+ }
+
+ protected void initialize() {
+ InputStream stream = getClass().getResourceAsStream(name);
+ if (stream != null) {
+ loadDocument(stream);
+ } else {
+ logger.warn("Resource stream {} cannot be found.", name);
+ }
+ }
+
+ public void loadDocument(String filename) {
+ try {
+ InputStream stream = new FileInputStream(filename);
+ loadDocument(stream);
+ } catch (FileNotFoundException e) {
+ logger.warn("xml document {} not found", filename);
+ }
+ }
+
+ protected void loadDocument(InputStream stream) {
+ try {
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
+ dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ dbFactory.setXIncludeAware(false);
+ dbFactory.setExpandEntityReferences(false);
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+ Document doc = dBuilder.parse(stream);
+ doc.getDocumentElement().normalize();
+
+ parseDocument(doc.getDocumentElement());
+ } catch (ParserConfigurationException e) {
+ logger.warn("parser config error when loading xml document:", e);
+ } catch (SAXException e) {
+ logger.warn("SAX exception when loading xml document:", e);
+ } catch (IOException e) {
+ logger.warn("I/O exception when loading xml document:", e);
+ }
+ }
+
+ protected abstract void parseDocument(Element element) throws SAXException;
+
+ protected Map getFlags(Element element) {
+ NamedNodeMap attributes = element.getAttributes();
+ Map flags = new HashMap<>();
+ for (int i = 0; i < attributes.getLength(); i++) {
+ Node attribute = attributes.item(i);
+ String nodeName = attribute.getNodeName();
+ String nodeValue = attribute.getNodeValue();
+ if ("true".equals(nodeValue) || "false".equals(nodeValue)) {
+ flags.put(nodeName, "true".equals(nodeValue));
+ }
+ }
+ return flags;
+ }
+
+ protected Map getParameters(Element element, List excludedAttrs) {
+ NamedNodeMap attributes = element.getAttributes();
+ Map params = new HashMap<>();
+ for (int i = 0; i < attributes.getLength(); i++) {
+ Node attribute = attributes.item(i);
+ String nodeName = attribute.getNodeName();
+ String nodeValue = attribute.getNodeValue();
+ if (!excludedAttrs.contains(nodeName)) {
+ params.put(nodeName, nodeValue);
+ }
+ }
+ return params;
+ }
+
+ protected int getAttributeAsInteger(Element element, String name) throws SAXException {
+ try {
+ return Integer.parseInt(element.getAttribute(name));
+ } catch (NumberFormatException e) {
+ throw new SAXException("invalid integer attribute " + name);
+ }
+ }
+
+ protected int getAttributeAsInteger(Element element, String name, int defaultValue) throws SAXException {
+ return element.hasAttribute(name) ? getAttributeAsInteger(element, name) : defaultValue;
+ }
+
+ protected int getHexAttributeAsInteger(Element element, String name) throws SAXException {
+ try {
+ return HexUtils.toInteger(element.getAttribute(name));
+ } catch (NumberFormatException e) {
+ throw new SAXException("invalid hex attribute " + name);
+ }
+ }
+
+ protected int getHexAttributeAsInteger(Element element, String name, int defaultValue) throws SAXException {
+ return element.hasAttribute(name) ? getHexAttributeAsInteger(element, name) : defaultValue;
+ }
+
+ protected byte getHexAttributeAsByte(Element element, String name) throws SAXException {
+ return (byte) (getHexAttributeAsInteger(element, name) & 0xFF);
+ }
+
+ protected byte getHexAttributeAsByte(Element element, String name, byte defaultValue) throws SAXException {
+ return element.hasAttribute(name) ? getHexAttributeAsByte(element, name) : defaultValue;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonStateDescriptionProvider.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonStateDescriptionProvider.java
new file mode 100644
index 0000000000000..86110bf38c4c2
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/InsteonStateDescriptionProvider.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.EventPublisher;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link InsteonStateDescriptionProvider} is a dynamic provider of state options for Insteon channels
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, InsteonStateDescriptionProvider.class })
+@NonNullByDefault
+public class InsteonStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
+
+ @Activate
+ public InsteonStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
+ final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
+ final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.eventPublisher = eventPublisher;
+ this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ChannelCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ChannelCommand.java
new file mode 100644
index 0000000000000..cba2e89147e8b
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ChannelCommand.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.StringsCompleter;
+
+/**
+ *
+ * The {@link ChannelCommand} represents an Insteon console channel command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class ChannelCommand extends InsteonCommand {
+ private static final String NAME = "channel";
+ private static final String DESCRIPTION = "Insteon channel commands";
+
+ private static final String LIST_ALL = "listAll";
+
+ private static final List SUBCMDS = List.of(LIST_ALL);
+
+ public ChannelCommand(InsteonCommandExtension commandExtension) {
+ super(NAME, DESCRIPTION, commandExtension);
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(buildCommandUsage(LIST_ALL, "list available channel ids with configuration and link state"));
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+
+ switch (args[0]) {
+ case LIST_ALL:
+ if (args.length == 1) {
+ listAll(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ List strings = List.of();
+ if (cursorArgumentIndex == 0) {
+ strings = SUBCMDS;
+ }
+
+ return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+
+ private void listAll(Console console) {
+ Map channels = Stream
+ .concat(Stream.of(getBridgeHandler()), getBridgeHandler().getChildHandlers())
+ .flatMap(handler -> handler.getChannelsInfo().entrySet().stream())
+ .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
+ if (channels.isEmpty()) {
+ console.println("No channel available!");
+ } else {
+ console.println("There are " + channels.size() + " channels available:");
+ print(console, channels);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DebugCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DebugCommand.java
new file mode 100644
index 0000000000000..d35e495b61c30
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DebugCommand.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.InsteonBindingConstants;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.InsteonScene;
+import org.openhab.binding.insteon.internal.device.X10Address;
+import org.openhab.binding.insteon.internal.device.X10Device;
+import org.openhab.binding.insteon.internal.transport.PortListener;
+import org.openhab.binding.insteon.internal.transport.message.FieldException;
+import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
+import org.openhab.binding.insteon.internal.transport.message.MsgDefinitionRegistry;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.StringsCompleter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * The {@link DebugCommand} represents an Insteon console debug command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class DebugCommand extends InsteonCommand implements PortListener {
+ private static final String NAME = "debug";
+ private static final String DESCRIPTION = "Insteon debug commands";
+
+ private static final String LIST_MONITORED = "listMonitored";
+ private static final String START_MONITORING = "startMonitoring";
+ private static final String STOP_MONITORING = "stopMonitoring";
+ private static final String SEND_BROADCAST_MESSAGE = "sendBroadcastMessage";
+ private static final String SEND_STANDARD_MESSAGE = "sendStandardMessage";
+ private static final String SEND_EXTENDED_MESSAGE = "sendExtendedMessage";
+ private static final String SEND_EXTENDED_2_MESSAGE = "sendExtended2Message";
+ private static final String SEND_X10_MESSAGE = "sendX10Message";
+ private static final String SEND_IM_MESSAGE = "sendIMMessage";
+
+ private static final List SUBCMDS = List.of(LIST_MONITORED, START_MONITORING, STOP_MONITORING,
+ SEND_BROADCAST_MESSAGE, SEND_STANDARD_MESSAGE, SEND_EXTENDED_MESSAGE, SEND_EXTENDED_2_MESSAGE,
+ SEND_X10_MESSAGE, SEND_IM_MESSAGE);
+
+ private static final String ALL_OPTION = "--all";
+
+ private static final String MSG_EVENTS_FILE_PREFIX = "messageEvents";
+
+ private static enum MessageType {
+ STANDARD,
+ EXTENDED,
+ EXTENDED_2
+ }
+
+ private final Logger logger = LoggerFactory.getLogger(DebugCommand.class);
+
+ private boolean monitoring = false;
+ private boolean monitorAllDevices = false;
+ private Set monitoredAddresses = new HashSet<>();
+
+ public DebugCommand(InsteonCommandExtension commandExtension) {
+ super(NAME, DESCRIPTION, commandExtension);
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(buildCommandUsage(LIST_MONITORED, "list monitored device(s)"),
+ buildCommandUsage(START_MONITORING + " " + ALL_OPTION + "|",
+ "start logging message events for device(s) in separate file(s)"),
+ buildCommandUsage(STOP_MONITORING + " " + ALL_OPTION + "|",
+ "stop logging message events for device(s) in separate file(s)"),
+ buildCommandUsage(SEND_BROADCAST_MESSAGE + " ",
+ "send an Insteon broadcast message to a group"),
+ buildCommandUsage(SEND_STANDARD_MESSAGE + " ",
+ "send an Insteon standard message to a device"),
+ buildCommandUsage(SEND_EXTENDED_MESSAGE + " [ ... ]",
+ "send an Insteon extended message with standard crc to a device"),
+ buildCommandUsage(SEND_EXTENDED_2_MESSAGE + " [ ... ]",
+ "send an Insteon extended message with a two-byte crc to a device"),
+ buildCommandUsage(SEND_X10_MESSAGE + " ", "send an X10 message to a device"),
+ buildCommandUsage(SEND_IM_MESSAGE + " [ ...]",
+ "send an IM message to the modem"));
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+
+ switch (args[0]) {
+ case LIST_MONITORED:
+ if (args.length == 1) {
+ listMonitoredDevices(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case START_MONITORING:
+ if (args.length == 2) {
+ startMonitoring(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case STOP_MONITORING:
+ if (args.length == 2) {
+ stopMonitoring(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_BROADCAST_MESSAGE:
+ if (args.length == 4) {
+ sendBroadcastMessage(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_STANDARD_MESSAGE:
+ if (args.length == 4) {
+ sendDirectMessage(console, MessageType.STANDARD, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_EXTENDED_MESSAGE:
+ if (args.length >= 4 && args.length <= 17) {
+ sendDirectMessage(console, MessageType.EXTENDED, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_EXTENDED_2_MESSAGE:
+ if (args.length >= 4 && args.length <= 16) {
+ sendDirectMessage(console, MessageType.EXTENDED_2, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_X10_MESSAGE:
+ if (args.length == 3) {
+ sendX10Message(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SEND_IM_MESSAGE:
+ if (args.length >= 2) {
+ sendIMMessage(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ List strings = List.of();
+ if (cursorArgumentIndex == 0) {
+ strings = SUBCMDS;
+ } else if (cursorArgumentIndex == 1) {
+ switch (args[0]) {
+ case START_MONITORING:
+ case STOP_MONITORING:
+ strings = Stream.concat(Stream.of(ALL_OPTION),
+ getModem().getDB().getDevices().stream().map(InsteonAddress::toString)).toList();
+ break;
+ case SEND_BROADCAST_MESSAGE:
+ strings = getModem().getDB().getBroadcastGroups().stream().map(String::valueOf).toList();
+ break;
+ case SEND_STANDARD_MESSAGE:
+ case SEND_EXTENDED_MESSAGE:
+ case SEND_EXTENDED_2_MESSAGE:
+ strings = getModem().getDB().getDevices().stream().map(InsteonAddress::toString).toList();
+ break;
+ case SEND_X10_MESSAGE:
+ strings = getModem().getX10Devices().stream().map(X10Device::getAddress).map(X10Address::toString)
+ .toList();
+ break;
+ case SEND_IM_MESSAGE:
+ strings = MsgDefinitionRegistry.getInstance().getDefinitions().entrySet().stream()
+ .filter(entry -> entry.getValue().getDirection() == Direction.TO_MODEM).map(Entry::getKey)
+ .toList();
+ break;
+ }
+ }
+
+ return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+
+ @Override
+ public void disconnected() {
+ // do nothing
+ }
+
+ @Override
+ public void messageReceived(Msg msg) {
+ try {
+ InsteonAddress address = msg.getInsteonAddress(msg.isReply() ? "toAddress" : "fromAddress");
+ if (monitorAllDevices || monitoredAddresses.contains(address)) {
+ logMessageEvent(address, msg);
+ }
+ } catch (FieldException ignored) {
+ // ignore message with no address field
+ }
+ }
+
+ @Override
+ public void messageSent(Msg msg) {
+ try {
+ InsteonAddress address = msg.getInsteonAddress("toAddress");
+ if (monitorAllDevices || monitoredAddresses.contains(address)) {
+ logMessageEvent(address, msg);
+ }
+ } catch (FieldException ignored) {
+ // ignore message with no address field
+ }
+ }
+
+ private String getMsgEventsFileName(String address) {
+ return MSG_EVENTS_FILE_PREFIX + "-" + address.replace(".", "") + ".log";
+ }
+
+ private String getMsgEventsFilePath(String address) {
+ return InsteonBindingConstants.BINDING_DATA_DIR + File.separator + getMsgEventsFileName(address);
+ }
+
+ private void clearMonitorFiles(String address) {
+ File folder = new File(InsteonBindingConstants.BINDING_DATA_DIR);
+ String prefix = ALL_OPTION.equals(address) ? MSG_EVENTS_FILE_PREFIX : getMsgEventsFileName(address);
+
+ if (folder.isDirectory()) {
+ Arrays.asList(folder.listFiles()).stream().filter(file -> file.getName().startsWith(prefix))
+ .forEach(File::delete);
+ }
+ }
+
+ private void logMessageEvent(InsteonAddress address, Msg msg) {
+ String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
+ String pathname = getMsgEventsFilePath(address.toString());
+
+ try {
+ File file = new File(pathname);
+ File parent = file.getParentFile();
+ if (parent == null) {
+ throw new IOException(pathname + " does not name a parent directory");
+ }
+ parent.mkdirs();
+ file.createNewFile();
+
+ PrintStream ps = new PrintStream(new FileOutputStream(file, true));
+ ps.println(timestamp + " " + msg.toString());
+ ps.close();
+ } catch (IOException e) {
+ logger.warn("failed to write to message event file", e);
+ }
+ }
+
+ private void listMonitoredDevices(Console console) {
+ String addresses = monitoredAddresses.stream().map(InsteonAddress::toString).collect(Collectors.joining(", "));
+ if (!addresses.isEmpty()) {
+ console.println("The monitored device(s) are: " + addresses);
+ } else if (monitorAllDevices) {
+ console.println("All devices are monitored.");
+ } else {
+ console.println("Not monitoring any devices.");
+ }
+ }
+
+ private void startMonitoring(Console console, String address) {
+ if (ALL_OPTION.equals(address)) {
+ if (!monitorAllDevices) {
+ monitorAllDevices = true;
+ monitoredAddresses.clear();
+ console.println("Started monitoring all devices.");
+ console.println("Message events logged in " + InsteonBindingConstants.BINDING_DATA_DIR);
+ clearMonitorFiles(address);
+ } else {
+ console.println("Already monitoring all devices.");
+ }
+ } else if (InsteonAddress.isValid(address)) {
+ if (monitorAllDevices) {
+ console.println("Already monitoring all devices.");
+ } else if (monitoredAddresses.add(new InsteonAddress(address))) {
+ console.println("Started monitoring the device " + address + ".");
+ console.println("Message events logged in " + getMsgEventsFilePath(address));
+ clearMonitorFiles(address);
+ } else {
+ console.println("Already monitoring the device " + address + ".");
+ }
+ } else {
+ console.println("Invalid device address" + address + ".");
+ return;
+ }
+
+ if (!monitoring) {
+ getModem().getPort().registerListener(this);
+ monitoring = true;
+ }
+ }
+
+ private void stopMonitoring(Console console, String address) {
+ if (!monitoring) {
+ console.println("Not monitoring any devices.");
+ return;
+ }
+
+ if (ALL_OPTION.equals(address)) {
+ if (monitorAllDevices) {
+ monitorAllDevices = false;
+ console.println("Stopped monitoring all devices.");
+ } else {
+ console.println("Not monitoring all devices.");
+ }
+ } else if (InsteonAddress.isValid(address)) {
+ if (monitorAllDevices) {
+ console.println("Not monitoring individual devices.");
+ } else if (monitoredAddresses.remove(new InsteonAddress(address))) {
+ console.println("Stopped monitoring the device " + address + ".");
+ } else {
+ console.println("Not monitoring the device " + address + ".");
+ return;
+ }
+ } else {
+ console.println("Invalid address device address " + address + ".");
+ return;
+ }
+
+ if (!monitorAllDevices && monitoredAddresses.isEmpty()) {
+ getModem().getPort().unregisterListener(this);
+ monitoring = false;
+ }
+ }
+
+ private void sendBroadcastMessage(Console console, String[] args) {
+ if (!InsteonScene.isValidGroup(args[1])) {
+ console.println("Invalid group argument: " + args[1]);
+ } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
+ console.println("Invalid hex argument(s).");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("Not ready to send messages yet.");
+ } else {
+ try {
+ int group = Integer.parseInt(args[1]);
+ byte cmd1 = (byte) HexUtils.toInteger(args[2]);
+ byte cmd2 = (byte) HexUtils.toInteger(args[3]);
+ Msg msg = Msg.makeBroadcastMessage(group, cmd1, cmd2);
+ getModem().writeMessage(msg);
+ console.println("Broadcast message sent to group " + group + ".");
+ console.println(msg.toString());
+ } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
+ console.println("Error while trying to create message.");
+ } catch (IOException e) {
+ console.println("Failed to send message.");
+ }
+ }
+ }
+
+ private void sendDirectMessage(Console console, MessageType messageType, String[] args) {
+ if (!InsteonAddress.isValid(args[1])) {
+ console.println("Invalid device address argument: " + args[1]);
+ } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
+ console.println("Invalid hex argument(s).");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("Not ready to send messages yet.");
+ } else {
+ try {
+ InsteonAddress address = new InsteonAddress(args[1]);
+ byte cmd1 = (byte) HexUtils.toInteger(args[2]);
+ byte cmd2 = (byte) HexUtils.toInteger(args[3]);
+ Msg msg;
+ if (messageType == MessageType.STANDARD) {
+ msg = Msg.makeStandardMessage(address, cmd1, cmd2);
+ } else {
+ byte[] data = HexUtils.toByteArray(args, 4, args.length);
+ boolean setCRC = getInsteonEngine(args[1]).supportsChecksum();
+ if (messageType == MessageType.EXTENDED) {
+ msg = Msg.makeExtendedMessage(address, cmd1, cmd2, data, setCRC);
+ } else {
+ msg = Msg.makeExtendedMessageCRC2(address, cmd1, cmd2, data);
+ }
+ }
+ getModem().writeMessage(msg);
+ console.println("Direct message sent to device " + address + ".");
+ console.println(msg.toString());
+ } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
+ console.println("Error while trying to create message.");
+ } catch (IOException e) {
+ console.println("Failed to send message.");
+ }
+ }
+ }
+
+ private void sendX10Message(Console console, String[] args) {
+ if (!X10Address.isValid(args[1])) {
+ console.println("Invalid x10 address argument: " + args[1]);
+ } else if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
+ console.println("Invalid hex argument(s).");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("Not ready to send messages yet.");
+ } else {
+ try {
+ X10Address address = new X10Address(args[1]);
+ byte cmd = (byte) HexUtils.toInteger(args[2]);
+ Msg maddr = Msg.makeX10AddressMessage(address);
+ getModem().writeMessage(maddr);
+ Msg mcmd = Msg.makeX10CommandMessage(cmd);
+ getModem().writeMessage(mcmd);
+ console.println("X10 message sent to device " + address + ".");
+ console.println(maddr.toString());
+ console.println(mcmd.toString());
+ } catch (FieldException | InvalidMessageTypeException | NumberFormatException e) {
+ console.println("Error while trying to create message.");
+ } catch (IOException e) {
+ console.println("Failed to send message.");
+ }
+ }
+ }
+
+ private void sendIMMessage(Console console, String[] args) {
+ if (!HexUtils.isValidHexStringArray(args, 2, args.length)) {
+ console.println("Invalid hex argument(s).");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("Not ready to send messages yet.");
+ } else {
+ try {
+ Msg msg = Msg.makeMessage(args[1]);
+ byte[] data = msg.getData();
+ int headerLength = msg.getHeaderLength();
+ for (int i = 0; i + 2 < args.length; i++) {
+ data[i + headerLength] = (byte) HexUtils.toInteger(args[i + 2]);
+ }
+ getModem().writeMessage(msg);
+ console.println("IM message sent to the modem.");
+ console.println(msg.toString());
+ } catch (ArrayIndexOutOfBoundsException e) {
+ console.println("Too many data bytes provided.");
+ } catch (InvalidMessageTypeException e) {
+ console.println("Error while trying to create message.");
+ } catch (IOException e) {
+ console.println("Failed to send message.");
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DeviceCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DeviceCommand.java
new file mode 100644
index 0000000000000..7538386516454
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/DeviceCommand.java
@@ -0,0 +1,719 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.device.Device;
+import org.openhab.binding.insteon.internal.device.DeviceFeature;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.InsteonDevice;
+import org.openhab.binding.insteon.internal.device.ProductData;
+import org.openhab.binding.insteon.internal.device.database.LinkDBRecord;
+import org.openhab.binding.insteon.internal.device.feature.FeatureEnums.KeypadButtonToggleMode;
+import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonThingHandler;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.StringsCompleter;
+
+/**
+ *
+ * The {@link DeviceCommand} represents an Insteon console device command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCommand extends InsteonCommand {
+ private static final String NAME = "device";
+ private static final String DESCRIPTION = "Insteon/X10 device commands";
+
+ private static final String LIST_ALL = "listAll";
+ private static final String LIST_DATABASE = "listDatabase";
+ private static final String LIST_FEATURES = "listFeatures";
+ private static final String LIST_PRODUCT_DATA = "listProductData";
+ private static final String LIST_MISSING_LINKS = "listMissingLinks";
+ private static final String ADD_MISSING_LINKS = "addMissingLinks";
+ private static final String ADD_DATABASE_CONTROLLER = "addDatabaseController";
+ private static final String ADD_DATABASE_RESPONDER = "addDatabaseResponder";
+ private static final String DELETE_DATABASE_CONTROLLER = "deleteDatabaseController";
+ private static final String DELETE_DATABASE_RESPONDER = "deleteDatabaseResponder";
+ private static final String APPLY_DATABASE_CHANGES = "applyDatabaseChanges";
+ private static final String CLEAR_DATABASE_CHANGES = "clearDatabaseChanges";
+ private static final String SET_BUTTON_RADIO_GROUP = "setButtonRadioGroup";
+ private static final String CLEAR_BUTTON_RADIO_GROUP = "clearButtonRadioGroup";
+ private static final String REFRESH = "refresh";
+
+ private static final List SUBCMDS = List.of(LIST_ALL, LIST_DATABASE, LIST_FEATURES, LIST_PRODUCT_DATA,
+ LIST_MISSING_LINKS, ADD_MISSING_LINKS, ADD_DATABASE_CONTROLLER, ADD_DATABASE_RESPONDER,
+ DELETE_DATABASE_CONTROLLER, DELETE_DATABASE_RESPONDER, APPLY_DATABASE_CHANGES, CLEAR_DATABASE_CHANGES,
+ SET_BUTTON_RADIO_GROUP, CLEAR_BUTTON_RADIO_GROUP, REFRESH);
+
+ private static final String ALL_OPTION = "--all";
+ private static final String CONFIRM_OPTION = "--confirm";
+
+ public DeviceCommand(InsteonCommandExtension commandExtension) {
+ super(NAME, DESCRIPTION, commandExtension);
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(
+ buildCommandUsage(LIST_ALL, "list configured Insteon/X10 devices with related channels and status"),
+ buildCommandUsage(LIST_DATABASE + " ",
+ "list all-link database records and pending changes for a configured Insteon device"),
+ buildCommandUsage(LIST_FEATURES + " ", "list features for a configured Insteon/X10 device"),
+ buildCommandUsage(LIST_PRODUCT_DATA + " ",
+ "list product data for a configured Insteon/X10 device"),
+ buildCommandUsage(LIST_MISSING_LINKS + " " + ALL_OPTION + "|",
+ "list missing links for a specific or all configured Insteon devices"),
+ buildCommandUsage(ADD_MISSING_LINKS + " " + ALL_OPTION + "|",
+ "add missing links for a specific or all configured Insteon devices"),
+ buildCommandUsage(ADD_DATABASE_CONTROLLER + " ",
+ "add a controller record to all-link database for a configured Insteon device"),
+ buildCommandUsage(ADD_DATABASE_RESPONDER + " ",
+ "add a responder record to all-link database for a configured Insteon device"),
+ buildCommandUsage(DELETE_DATABASE_CONTROLLER + " ",
+ "delete a controller record from all-link database for a configured Insteon device"),
+ buildCommandUsage(DELETE_DATABASE_RESPONDER + " ",
+ "delete a responder record from all-link database for a configured Insteon device"),
+ buildCommandUsage(APPLY_DATABASE_CHANGES + " " + CONFIRM_OPTION,
+ "apply all-link database pending changes for a configured Insteon device"),
+ buildCommandUsage(CLEAR_DATABASE_CHANGES + " ",
+ "clear all-link database pending changes for a configured Insteon device"),
+ buildCommandUsage(SET_BUTTON_RADIO_GROUP + " [ ... ]",
+ "set a button radio group for a configured Insteon KeypadLinc device"),
+ buildCommandUsage(CLEAR_BUTTON_RADIO_GROUP + " [ ... ]",
+ "clear a button radio group for a configured Insteon KeypadLinc device"),
+ buildCommandUsage(REFRESH + " ", "refresh data for a configured Insteon device"));
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+
+ switch (args[0]) {
+ case LIST_ALL:
+ if (args.length == 1) {
+ listAll(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_DATABASE:
+ if (args.length == 2) {
+ listDatabaseRecords(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_FEATURES:
+ if (args.length == 2) {
+ listFeatures(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_PRODUCT_DATA:
+ if (args.length == 2) {
+ listProductData(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_MISSING_LINKS:
+ if (args.length == 2) {
+ if (ALL_OPTION.equals(args[1])) {
+ listMissingLinks(console);
+ } else {
+ listMissingLinks(console, args[1]);
+ }
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_MISSING_LINKS:
+ if (args.length == 2) {
+ if (ALL_OPTION.equals(args[1])) {
+ addMissingLinks(console);
+ } else {
+ addMissingLinks(console, args[1]);
+ }
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DATABASE_CONTROLLER:
+ if (args.length == 7) {
+ addDatabaseRecord(console, args, true);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DATABASE_RESPONDER:
+ if (args.length == 7) {
+ addDatabaseRecord(console, args, false);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case DELETE_DATABASE_CONTROLLER:
+ if (args.length == 5) {
+ deleteDatabaseRecord(console, args, true);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case DELETE_DATABASE_RESPONDER:
+ if (args.length == 5) {
+ deleteDatabaseRecord(console, args, false);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case APPLY_DATABASE_CHANGES:
+ if (args.length == 2 || args.length == 3 && CONFIRM_OPTION.equals(args[2])) {
+ applyDatabaseChanges(console, args[1], args.length == 3);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case CLEAR_DATABASE_CHANGES:
+ if (args.length == 2) {
+ clearDatabaseChanges(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SET_BUTTON_RADIO_GROUP:
+ if (args.length >= 4 && args.length <= 9) {
+ setButtonRadioGroup(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case CLEAR_BUTTON_RADIO_GROUP:
+ if (args.length >= 4 && args.length <= 9) {
+ clearButtonRadioGroup(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case REFRESH:
+ if (args.length == 2) {
+ refreshDevice(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ List strings = List.of();
+ if (cursorArgumentIndex == 0) {
+ strings = SUBCMDS;
+ } else if (cursorArgumentIndex == 1) {
+ switch (args[0]) {
+ case LIST_FEATURES:
+ case LIST_PRODUCT_DATA:
+ strings = getAllDeviceHandlers().map(InsteonThingHandler::getThingId).toList();
+ break;
+ case LIST_DATABASE:
+ case REFRESH:
+ strings = getInsteonDeviceHandlers().map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ case ADD_DATABASE_CONTROLLER:
+ case DELETE_DATABASE_CONTROLLER:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && !device.getControllerFeatures().isEmpty();
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ case ADD_DATABASE_RESPONDER:
+ case DELETE_DATABASE_RESPONDER:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && !device.getResponderFeatures().isEmpty();
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ case APPLY_DATABASE_CHANGES:
+ case CLEAR_DATABASE_CHANGES:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && !device.getLinkDB().getChanges().isEmpty();
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ case LIST_MISSING_LINKS:
+ case ADD_MISSING_LINKS:
+ strings = Stream.concat(Stream.of(ALL_OPTION),
+ getInsteonDeviceHandlers().map(InsteonDeviceHandler::getThingId)).toList();
+ break;
+ case SET_BUTTON_RADIO_GROUP:
+ case CLEAR_BUTTON_RADIO_GROUP:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && !device.getFeatures(FEATURE_TYPE_KEYPAD_BUTTON).isEmpty();
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ }
+ } else if (cursorArgumentIndex == 2) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ switch (args[0]) {
+ case ADD_DATABASE_CONTROLLER:
+ case ADD_DATABASE_RESPONDER:
+ if (device != null) {
+ strings = Stream
+ .concat(Stream.of(getModem().getAddress()),
+ getModem().getDB().getDevices().stream()
+ .filter(address -> !device.getAddress().equals(address)))
+ .map(InsteonAddress::toString).toList();
+ }
+ break;
+ case DELETE_DATABASE_CONTROLLER:
+ if (device != null) {
+ strings = device.getLinkDB().getControllerRecords().stream()
+ .map(record -> record.getAddress().toString()).distinct().toList();
+ }
+ break;
+ case DELETE_DATABASE_RESPONDER:
+ if (device != null) {
+ strings = device.getLinkDB().getResponderRecords().stream()
+ .map(record -> record.getAddress().toString()).distinct().toList();
+ }
+ break;
+ case APPLY_DATABASE_CHANGES:
+ strings = List.of(CONFIRM_OPTION);
+ break;
+ }
+ } else if (cursorArgumentIndex == 3) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ InsteonAddress address = InsteonAddress.isValid(args[2]) ? new InsteonAddress(args[2]) : null;
+ switch (args[0]) {
+ case DELETE_DATABASE_CONTROLLER:
+ if (device != null && address != null) {
+ strings = device.getLinkDB().getControllerRecords(address).stream()
+ .map(record -> HexUtils.getHexString(record.getGroup())).distinct().toList();
+ }
+ break;
+ case DELETE_DATABASE_RESPONDER:
+ if (device != null && address != null) {
+ strings = device.getLinkDB().getResponderRecords(address).stream()
+ .map(record -> HexUtils.getHexString(record.getGroup())).distinct().toList();
+ }
+ break;
+ }
+ } else if (cursorArgumentIndex == 4) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ InsteonAddress address = InsteonAddress.isValid(args[2]) ? new InsteonAddress(args[2]) : null;
+ int group = HexUtils.isValidHexString(args[3]) ? HexUtils.toInteger(args[3]) : -1;
+ switch (args[0]) {
+ case DELETE_DATABASE_CONTROLLER:
+ if (device != null && address != null && group != -1) {
+ strings = device.getLinkDB().getControllerRecords(address, group).stream()
+ .map(record -> HexUtils.getHexString(record.getComponentId())).distinct().toList();
+ }
+ break;
+ case DELETE_DATABASE_RESPONDER:
+ if (device != null && address != null && group != -1) {
+ strings = device.getLinkDB().getResponderRecords(address, group).stream()
+ .map(record -> HexUtils.getHexString(record.getComponentId())).distinct().toList();
+ }
+ break;
+ }
+ }
+
+ if (cursorArgumentIndex >= 2) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ switch (args[0]) {
+ case SET_BUTTON_RADIO_GROUP:
+ case CLEAR_BUTTON_RADIO_GROUP:
+ if (device != null) {
+ strings = device.getFeatures(FEATURE_TYPE_KEYPAD_BUTTON).stream().map(DeviceFeature::getName)
+ .filter(name -> !Arrays.asList(args).subList(2, cursorArgumentIndex).contains(name))
+ .toList();
+ }
+ break;
+ }
+ }
+
+ return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+
+ private void listAll(Console console) {
+ Map devices = getAllDeviceHandlers()
+ .collect(Collectors.toMap(InsteonThingHandler::getThingId, InsteonThingHandler::getThingInfo));
+ if (devices.isEmpty()) {
+ console.println("No device configured or enabled!");
+ } else {
+ console.println("There are " + devices.size() + " devices configured:");
+ print(console, devices);
+ }
+ }
+
+ private void listDatabaseRecords(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ return;
+ }
+ List records = device.getLinkDB().getRecords().stream().map(String::valueOf).toList();
+ if (records.isEmpty()) {
+ console.println("The all-link database for device " + device.getAddress() + " is empty");
+ } else {
+ console.println("The all-link database for device " + device.getAddress() + " contains " + records.size()
+ + " records:" + (!device.getLinkDB().isComplete() ? " (Partial)" : ""));
+ print(console, records);
+ listDatabaseChanges(console, thingId);
+ }
+ }
+
+ private void listDatabaseChanges(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ return;
+ }
+ List changes = device.getLinkDB().getChanges().stream().map(String::valueOf).toList();
+ if (!changes.isEmpty()) {
+ console.println("The all-link database for device " + device.getAddress() + " has " + changes.size()
+ + " pending changes:");
+ print(console, changes);
+ }
+ }
+
+ private void listFeatures(Console console, String thingId) {
+ Device device = getDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ return;
+ }
+ List features = device.getFeatures().stream()
+ .filter(feature -> !feature.isEventFeature() && !feature.isGroupFeature())
+ .map(feature -> String.format("%s: type=%s state=%s isHidden=%s", feature.getName(), feature.getType(),
+ feature.getState().toFullString(), feature.isHiddenFeature()))
+ .sorted().toList();
+ if (features.isEmpty()) {
+ console.println("The features for device " + device.getAddress() + " are not defined");
+ } else {
+ console.println("The features for device " + device.getAddress() + " are:");
+ print(console, features);
+ }
+ }
+
+ private void listProductData(Console console, String thingId) {
+ Device device = getDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ return;
+ }
+ ProductData productData = device.getProductData();
+ if (productData == null) {
+ console.println("The product data for device " + device.getAddress() + " is not defined");
+ } else {
+ console.println("The product data for device " + device.getAddress() + " is:");
+ console.println(productData.toString().replace("|", "\n"));
+ }
+ }
+
+ private void listMissingLinks(Console console) {
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else {
+ getInsteonDeviceHandlers().forEach(handler -> listMissingLinks(console, handler.getThingId()));
+ }
+ }
+
+ private void listMissingLinks(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ } else if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + thingId + " is not loaded yet.");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else {
+ List deviceLinks = device.getMissingDeviceLinks().entrySet().stream()
+ .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue().getRecord())).toList();
+ List modemLinks = device.getMissingModemLinks().entrySet().stream()
+ .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue().getRecord())).toList();
+ if (deviceLinks.isEmpty() && modemLinks.isEmpty()) {
+ console.println("There are no missing links for device " + device.getAddress() + ".");
+ } else {
+ if (!deviceLinks.isEmpty()) {
+ console.println("There are " + deviceLinks.size()
+ + " missing links from the link database for device " + device.getAddress() + ":");
+ print(console, deviceLinks);
+ }
+ if (!modemLinks.isEmpty()) {
+ console.println("There are " + modemLinks.size()
+ + " missing links from the modem database for device " + device.getAddress() + ":");
+ print(console, modemLinks);
+ }
+ }
+ }
+ }
+
+ private void addMissingLinks(Console console) {
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else {
+ getInsteonDeviceHandlers().forEach(handler -> addMissingLinks(console, handler.getThingId()));
+ }
+ }
+
+ private void addMissingLinks(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ } else if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + thingId + " is not loaded yet.");
+ } else if (!device.getLinkDB().getChanges().isEmpty()) {
+ console.println("The link database for device " + thingId + " has pending changes.");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (!getModem().getDB().getChanges().isEmpty()) {
+ console.println("The modem database has pending changes.");
+ } else {
+ int deviceLinkCount = device.getMissingDeviceLinks().size();
+ int modemLinkCount = device.getMissingModemLinks().size();
+ if (deviceLinkCount == 0 && modemLinkCount == 0) {
+ console.println("There are no missing links for device " + device.getAddress() + ".");
+ } else {
+ if (deviceLinkCount > 0) {
+ if (!device.isAwake() || !device.isResponding()) {
+ console.println("Scheduling " + deviceLinkCount + " missing links for device "
+ + device.getAddress() + " to be added to its link database the next time it is "
+ + (device.isBatteryPowered() ? "awake" : "responding") + ".");
+ } else {
+ console.println("Adding " + deviceLinkCount + " missing links for device " + device.getAddress()
+ + " to its link database...");
+ }
+ device.addMissingDeviceLinks();
+ }
+ if (modemLinkCount > 0) {
+ console.println("Adding " + modemLinkCount + " missing links for device " + device.getAddress()
+ + " to the modem database...");
+ device.addMissingModemLinks();
+ }
+ }
+ }
+ }
+
+ private void addDatabaseRecord(Console console, String[] args, boolean isController) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ if (device == null) {
+ console.println("The device " + args[1] + " is not configured or enabled!");
+ } else if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + args[1] + " is not loaded yet.");
+ } else if (!InsteonAddress.isValid(args[2])) {
+ console.println("Invalid record address argument: " + args[2]);
+ } else if (!HexUtils.isValidHexString(args[3])) {
+ console.println("Invalid record group hex argument: " + args[3]);
+ } else if (!HexUtils.isValidHexStringArray(args, 4, args.length)) {
+ console.println("Invalid record data hex argument(s).");
+ } else {
+ InsteonAddress address = new InsteonAddress(args[2]);
+ int group = HexUtils.toInteger(args[3]);
+ byte[] data = HexUtils.toByteArray(args, 4, args.length);
+
+ LinkDBRecord record = device.getLinkDB().getActiveRecord(address, group, isController, data[2]);
+ if (record == null) {
+ device.getLinkDB().markRecordForAdd(address, group, isController, data);
+ console.println("Added a pending change to add link database "
+ + (isController ? "controller" : "responder") + " record with address " + address
+ + " and group " + group + " for device " + device.getAddress() + ".");
+ } else {
+ device.getLinkDB().markRecordForModify(record, data);
+ console.println("Added a pending change to modify link database record located at "
+ + HexUtils.getHexString(record.getLocation(), 4) + " for device " + device.getAddress() + ".");
+ }
+ }
+ }
+
+ private void deleteDatabaseRecord(Console console, String[] args, boolean isController) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ if (device == null) {
+ console.println("The device " + args[1] + " is not configured or enabled!");
+ } else if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + args[1] + " is not loaded yet.");
+ } else if (!InsteonAddress.isValid(args[2])) {
+ console.println("Invalid record address argument: " + args[2]);
+ } else if (!HexUtils.isValidHexString(args[3])) {
+ console.println("Invalid record group hex argument: " + args[3]);
+ } else if (!HexUtils.isValidHexString(args[4])) {
+ console.println("Invalid record data3 hex argument: " + args[4]);
+ } else {
+ InsteonAddress address = new InsteonAddress(args[2]);
+ int group = HexUtils.toInteger(args[3]);
+ int componentId = HexUtils.toInteger(args[4]); // data3 as component id
+
+ LinkDBRecord record = device.getLinkDB().getActiveRecord(address, group, isController, componentId);
+ if (record == null) {
+ console.println("No link database " + (isController ? "controller" : "responder")
+ + " record with address " + address + " and group " + group + " to delete for device "
+ + device.getAddress() + ".");
+ } else {
+ device.getLinkDB().markRecordForDelete(record);
+ console.println("Added a pending change to delete link database record located at "
+ + HexUtils.getHexString(record.getLocation(), 4) + " for device " + device.getAddress() + ".");
+ }
+ }
+ }
+
+ private void applyDatabaseChanges(Console console, String thingId, boolean isConfirmed) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ } else if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + thingId + " is not loaded yet.");
+ } else if (device.getLinkDB().getChanges().isEmpty()) {
+ console.println("The link database for device " + thingId + " has no pending changes.");
+ } else if (!isConfirmed) {
+ listDatabaseChanges(console, thingId);
+ console.println("Please run the same command with " + CONFIRM_OPTION
+ + " option to have these changes written to the link database for device " + device.getAddress()
+ + ".");
+ } else {
+ int count = device.getLinkDB().getChanges().size();
+ if (!device.isAwake() || !device.isResponding() || !getModem().getDB().isComplete()) {
+ console.println("Scheduling " + count + " pending changes for device " + device.getAddress()
+ + " to be applied to its link database the next time it is "
+ + (device.isBatteryPowered() ? "awake" : "responding") + ".");
+ } else {
+ console.println("Applying " + count + " pending changes to link database for device "
+ + device.getAddress() + "...");
+ }
+ device.getLinkDB().update();
+ }
+ }
+
+ private void clearDatabaseChanges(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ } else if (device.getLinkDB().getChanges().isEmpty()) {
+ console.println("The link database for device " + thingId + " has no pending changes.");
+ } else {
+ int count = device.getLinkDB().getChanges().size();
+ device.getLinkDB().clearChanges();
+ console.println(
+ "Cleared " + count + " pending changes from link database for device " + device.getAddress() + ".");
+ }
+ }
+
+ private void setButtonRadioGroup(Console console, String[] args) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ if (device == null) {
+ console.println("The device " + args[1] + " is not configured or enabled!");
+ } else if (device.getFeatures(FEATURE_TYPE_KEYPAD_BUTTON).isEmpty()) {
+ console.println("The device " + args[1] + " does not have keypad buttons.");
+ } else {
+ List buttons = new ArrayList<>();
+ for (int i = 2; i < args.length; i++) {
+ DeviceFeature feature = device.getFeature(args[i]);
+ if (feature == null || !feature.getType().equals(FEATURE_TYPE_KEYPAD_BUTTON)) {
+ console.println("The feature " + args[i] + " is not configured or a keypad button.");
+ return;
+ }
+ int group = feature.getGroup();
+ if (!buttons.contains(group)) {
+ buttons.add(group);
+ }
+ }
+ if (buttons.size() < 2) {
+ console.println("Requires at least two buttons to set a radio group.");
+ return;
+ }
+
+ console.println("Setting a radio group for device " + device.getAddress() + "...");
+ device.setButtonRadioGroup(buttons);
+ device.setButtonToggleMode(buttons, KeypadButtonToggleMode.ALWAYS_ON);
+ }
+ }
+
+ private void clearButtonRadioGroup(Console console, String[] args) {
+ InsteonDevice device = getInsteonDevice(args[1]);
+ if (device == null) {
+ console.println("The device " + args[1] + " is not configured or enabled!");
+ } else if (device.getFeatures(FEATURE_TYPE_KEYPAD_BUTTON).isEmpty()) {
+ console.println("The device " + args[1] + " does not have keypad buttons.");
+ } else {
+ List buttons = new ArrayList<>();
+ for (int i = 2; i < args.length; i++) {
+ DeviceFeature feature = device.getFeature(args[i]);
+ if (feature == null || !feature.getType().equals(FEATURE_TYPE_KEYPAD_BUTTON)) {
+ console.println(
+ "The device " + args[1] + " feature " + args[i] + " is not configured or a keypad button.");
+ return;
+ }
+ int group = feature.getGroup();
+ int offMask = device.getLastMsgValueAsInteger(FEATURE_TYPE_KEYPAD_BUTTON_OFF_MASK, group, 0);
+ if (offMask == 0) {
+ console.println("The keypad button " + args[i] + " is not part of a radio group.");
+ return;
+ }
+ if (!buttons.contains(group)) {
+ buttons.add(group);
+ }
+ }
+ if (buttons.size() < 2) {
+ console.println("Requires at least two buttons to clear a radio group.");
+ return;
+ }
+
+ console.println("Clearing a radio group for device " + device.getAddress() + "...");
+ device.clearButtonRadioGroup(buttons);
+ device.setButtonToggleMode(buttons, KeypadButtonToggleMode.TOGGLE);
+ }
+ }
+
+ private void refreshDevice(Console console, String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ if (device == null) {
+ console.println("The device " + thingId + " is not configured or enabled!");
+ } else if (device.getProductData() == null) {
+ console.println("The device " + thingId + " is unknown.");
+ } else if (device.getType() == null) {
+ console.println("The device " + thingId + " is unsupported.");
+ } else {
+ device.getLinkDB().setReload(true);
+ device.resetFeaturesQueryStatus();
+
+ if (!device.isAwake() || !device.isResponding() || !getModem().getDB().isComplete()) {
+ console.println(
+ "The device " + device.getAddress() + " is scheduled to be refreshed the next time it is "
+ + (device.isBatteryPowered() ? "awake" : "responding") + ".");
+ } else {
+ console.println("Refreshing device " + device.getAddress() + "...");
+ device.doPoll(0L);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommand.java
new file mode 100644
index 0000000000000..7c068067fa294
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommand.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.Device;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.InsteonDevice;
+import org.openhab.binding.insteon.internal.device.InsteonEngine;
+import org.openhab.binding.insteon.internal.device.InsteonModem;
+import org.openhab.binding.insteon.internal.device.InsteonScene;
+import org.openhab.binding.insteon.internal.device.X10Address;
+import org.openhab.binding.insteon.internal.device.X10Device;
+import org.openhab.binding.insteon.internal.handler.InsteonBridgeHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonSceneHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonThingHandler;
+import org.openhab.binding.insteon.internal.handler.X10DeviceHandler;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.ConsoleCommandCompleter;
+
+/**
+ *
+ * The {@link InsteonCommand} represents a base Insteon console command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public abstract class InsteonCommand implements ConsoleCommandCompleter {
+ private final String name;
+ private final String description;
+ private final InsteonCommandExtension commandExtension;
+
+ public InsteonCommand(String name, String description, InsteonCommandExtension commandExtension) {
+ this.name = name;
+ this.description = description;
+ this.commandExtension = commandExtension;
+ }
+
+ public String getCommand() {
+ return commandExtension.getCommand();
+ }
+
+ public String getSubCommand() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public abstract List getUsages();
+
+ public abstract void execute(String[] args, Console console);
+
+ protected String buildCommandUsage(final String syntax, final String description) {
+ return String.format("%s %s %s - %s", getCommand(), getSubCommand(), syntax, description);
+ }
+
+ protected void printUsage(Console console) {
+ getUsages().forEach(console::printUsage);
+ }
+
+ protected void printUsage(Console console, String cmd) {
+ getUsages().stream().filter(usage -> usage.split(" ")[2].equals(cmd)).findAny().ifPresent(console::printUsage);
+ }
+
+ protected void print(Console console, List list) {
+ list.forEach(console::println);
+ }
+
+ protected void print(Console console, Map map) {
+ map.entrySet().stream().sorted(Entry.comparingByKey()).map(Entry::getValue).forEach(console::println);
+ }
+
+ protected InsteonBridgeHandler getBridgeHandler() {
+ return Objects.requireNonNull(commandExtension.getBridgeHandler());
+ }
+
+ protected @Nullable InsteonBridgeHandler getBridgeHandler(String thingId) {
+ return getBridgeHandlers().filter(handler -> handler.getThingId().equals(thingId)).findFirst().orElse(null);
+ }
+
+ protected Stream getBridgeHandlers() {
+ return commandExtension.getBridgeHandlers();
+ }
+
+ protected void setBridgeHandler(InsteonBridgeHandler handler) {
+ commandExtension.setBridgeHandler(handler);
+ }
+
+ protected Stream getAllDeviceHandlers() {
+ return Stream.concat(getInsteonDeviceHandlers(), getX10DeviceHandlers());
+ }
+
+ protected Stream getInsteonDeviceHandlers() {
+ return getBridgeHandler().getChildHandlers().filter(InsteonDeviceHandler.class::isInstance)
+ .map(InsteonDeviceHandler.class::cast);
+ }
+
+ protected Stream getX10DeviceHandlers() {
+ return getBridgeHandler().getChildHandlers().filter(X10DeviceHandler.class::isInstance)
+ .map(X10DeviceHandler.class::cast);
+ }
+
+ protected Stream getInsteonSceneHandlers() {
+ return getBridgeHandler().getChildHandlers().filter(InsteonSceneHandler.class::isInstance)
+ .map(InsteonSceneHandler.class::cast);
+ }
+
+ protected InsteonModem getModem() {
+ return Objects.requireNonNull(getBridgeHandler().getModem());
+ }
+
+ protected @Nullable Device getDevice(String thingId) {
+ if (InsteonAddress.isValid(thingId)) {
+ return getModem().getDevice(new InsteonAddress(thingId));
+ } else if (X10Address.isValid(thingId)) {
+ return getModem().getDevice(new X10Address(thingId));
+ } else {
+ return getAllDeviceHandlers().filter(handler -> handler.getThingId().equals(thingId))
+ .map(InsteonThingHandler::getDevice).findFirst().orElse(null);
+ }
+ }
+
+ protected @Nullable InsteonDevice getInsteonDevice(String thingId) {
+ if (InsteonAddress.isValid(thingId)) {
+ return getModem().getInsteonDevice(new InsteonAddress(thingId));
+ } else {
+ return getInsteonDeviceHandlers().filter(handler -> handler.getThingId().equals(thingId))
+ .map(InsteonDeviceHandler::getDevice).findFirst().orElse(null);
+ }
+ }
+
+ protected @Nullable X10Device getX10Device(String thingId) {
+ if (X10Address.isValid(thingId)) {
+ return getModem().getX10Device(new X10Address(thingId));
+ } else {
+ return getX10DeviceHandlers().filter(handler -> handler.getThingId().equals(thingId))
+ .map(X10DeviceHandler::getDevice).findFirst().orElse(null);
+ }
+ }
+
+ protected @Nullable InsteonScene getInsteonScene(String thingId) {
+ if (InsteonScene.isValidGroup(thingId)) {
+ return getModem().getInsteonScene(Integer.parseInt(thingId));
+ } else {
+ return getInsteonSceneHandlers().filter(handler -> handler.getThingId().equals(thingId))
+ .map(InsteonSceneHandler::getScene).findFirst().orElse(null);
+ }
+ }
+
+ protected InsteonEngine getInsteonEngine(String thingId) {
+ InsteonDevice device = getInsteonDevice(thingId);
+ return device != null ? device.getInsteonEngine() : InsteonEngine.UNKNOWN;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommandExtension.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommandExtension.java
index 6dc10918cf9a1..a28af4f1a2fe4 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommandExtension.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonCommandExtension.java
@@ -12,336 +12,148 @@
*/
package org.openhab.binding.insteon.internal.command;
-import java.text.SimpleDateFormat;
+import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
-import java.util.Date;
-import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Set;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.insteon.internal.InsteonBinding;
-import org.openhab.binding.insteon.internal.device.DeviceFeature;
-import org.openhab.binding.insteon.internal.device.InsteonAddress;
-import org.openhab.binding.insteon.internal.device.InsteonDevice;
-import org.openhab.binding.insteon.internal.handler.InsteonNetworkHandler;
-import org.openhab.binding.insteon.internal.message.FieldException;
-import org.openhab.binding.insteon.internal.message.InvalidMessageTypeException;
-import org.openhab.binding.insteon.internal.message.Msg;
-import org.openhab.binding.insteon.internal.message.MsgListener;
-import org.openhab.binding.insteon.internal.utils.Utils;
+import org.openhab.binding.insteon.internal.InsteonBindingConstants;
+import org.openhab.binding.insteon.internal.handler.InsteonBridgeHandler;
import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.ConsoleCommandCompleter;
+import org.openhab.core.io.console.StringsCompleter;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
-import org.osgi.service.component.annotations.ReferenceCardinality;
-import org.osgi.service.component.annotations.ReferencePolicy;
-import org.osgi.service.component.annotations.ReferencePolicyOption;
/**
*
- * Console commands for the Insteon binding
+ * The {@link InsteonCommandExtension} is responsible for handling console commands
*
- * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
-public class InsteonCommandExtension extends AbstractConsoleCommandExtension implements MsgListener {
- private static final String DISPLAY_DEVICES = "display_devices";
- private static final String DISPLAY_CHANNELS = "display_channels";
- private static final String DISPLAY_LOCAL_DATABASE = "display_local_database";
- private static final String DISPLAY_MONITORED = "display_monitored";
- private static final String START_MONITORING = "start_monitoring";
- private static final String STOP_MONITORING = "stop_monitoring";
- private static final String SEND_STANDARD_MESSAGE = "send_standard_message";
- private static final String SEND_EXTENDED_MESSAGE = "send_extended_message";
- private static final String SEND_EXTENDED_MESSAGE_2 = "send_extended_message_2";
+public class InsteonCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
- private enum MessageType {
- STANDARD,
- EXTENDED,
- EXTENDED_2
- }
+ private static final List> SUBCMD_CLASSES = List.of(ModemCommand.class,
+ DeviceCommand.class, SceneCommand.class, ChannelCommand.class, DebugCommand.class);
- @Nullable
- private InsteonNetworkHandler handler;
- @Nullable
- private Console console;
- private boolean monitoring = false;
- private boolean monitorAllDevices = false;
- private Set monitoredAddresses = new HashSet<>();
+ private final ThingRegistry thingRegistry;
+ private final InsteonLegacyCommandExtension legacyCommandExtension;
+ private final Map subCommands;
- public InsteonCommandExtension() {
- super("insteon", "Interact with the Insteon integration.");
- }
+ private @Nullable InsteonBridgeHandler handler;
- @Override
- public void execute(String[] args, Console console) {
- if (args.length > 0) {
- InsteonNetworkHandler handler = this.handler; // fix eclipse warnings about nullable
- if (handler == null) {
- console.println("No Insteon network bridge configured.");
- } else {
- switch (args[0]) {
- case DISPLAY_DEVICES:
- if (args.length == 1) {
- handler.displayDevices(console);
- } else {
- printUsage(console);
- }
- break;
- case DISPLAY_CHANNELS:
- if (args.length == 1) {
- handler.displayChannels(console);
- } else {
- printUsage(console);
- }
- break;
- case DISPLAY_LOCAL_DATABASE:
- if (args.length == 1) {
- handler.displayLocalDatabase(console);
- } else {
- printUsage(console);
- }
- break;
- case DISPLAY_MONITORED:
- if (args.length == 1) {
- displayMonitoredDevices(console);
- } else {
- printUsage(console);
- }
- break;
- case START_MONITORING:
- if (args.length == 2) {
- startMonitoring(console, args[1]);
- } else {
- printUsage(console);
- }
- break;
- case STOP_MONITORING:
- if (args.length == 2) {
- stopMonitoring(console, args[1]);
- } else {
- printUsage(console);
- }
- break;
- case SEND_STANDARD_MESSAGE:
- if (args.length == 5) {
- sendMessage(console, MessageType.STANDARD, args);
- } else {
- printUsage(console);
- }
- break;
- case SEND_EXTENDED_MESSAGE:
- if (args.length >= 5 && args.length <= 18) {
- sendMessage(console, MessageType.EXTENDED, args);
- } else {
- printUsage(console);
- }
- break;
- case SEND_EXTENDED_MESSAGE_2:
- if (args.length >= 5 && args.length <= 17) {
- sendMessage(console, MessageType.EXTENDED_2, args);
- } else {
- printUsage(console);
- }
- break;
- default:
- console.println("Unknown command '" + args[0] + "'");
- printUsage(console);
- break;
- }
- }
- } else {
- printUsage(console);
- }
- }
+ @Activate
+ public InsteonCommandExtension(final @Reference ThingRegistry thingRegistry) {
+ super(InsteonBindingConstants.BINDING_ID, "Interact with the Insteon integration.");
+ this.thingRegistry = thingRegistry;
- @Override
- public List getUsages() {
- return Arrays.asList(new String[] {
- buildCommandUsage(DISPLAY_DEVICES, "display devices that are online, along with available channels"),
- buildCommandUsage(DISPLAY_CHANNELS,
- "display channels that are linked, along with configuration information"),
- buildCommandUsage(DISPLAY_LOCAL_DATABASE, "display Insteon PLM or hub database details"),
- buildCommandUsage(DISPLAY_MONITORED, "display monitored device(s)"),
- buildCommandUsage(START_MONITORING + " all|address",
- "start displaying messages received from device(s)"),
- buildCommandUsage(STOP_MONITORING + " all|address", "stop displaying messages received from device(s)"),
- buildCommandUsage(SEND_STANDARD_MESSAGE + " address flags cmd1 cmd2",
- "send standard message to a device"),
- buildCommandUsage(SEND_EXTENDED_MESSAGE + " address flags cmd1 cmd2 [up to 13 bytes]",
- "send extended message to a device"),
- buildCommandUsage(SEND_EXTENDED_MESSAGE_2 + " address flags cmd1 cmd2 [up to 12 bytes]",
- "send extended message with a two byte crc to a device") });
+ this.legacyCommandExtension = new InsteonLegacyCommandExtension(thingRegistry);
+ this.subCommands = SUBCMD_CLASSES.stream().map(this::instantiateCommand).filter(Objects::nonNull)
+ .collect(Collectors.toMap(InsteonCommand::getSubCommand, Function.identity(), (key1, key2) -> key1,
+ LinkedHashMap::new));
}
@Override
- public void msg(Msg msg) {
- if (monitorAllDevices || monitoredAddresses.contains(msg.getAddr("fromAddress"))) {
- String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
- Console console = this.console;
- if (console != null) {
- console.println(date + " " + msg.toString());
- }
+ public List getUsages() {
+ if (legacyCommandExtension.isAvailable()) {
+ return legacyCommandExtension.getUsages();
}
+ return subCommands.values().stream().map(cmd -> buildCommandUsage(cmd.getSubCommand(), cmd.getDescription()))
+ .toList();
}
- @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY)
- public void setInsteonNetworkHandler(InsteonNetworkHandler handler) {
- this.handler = handler;
- }
-
- public void unsetInsteonNetworkHandler(InsteonNetworkHandler handler) {
- this.handler = null;
+ @Override
+ public @Nullable ConsoleCommandCompleter getCompleter() {
+ return this;
}
- private void displayMonitoredDevices(Console console) {
- if (!monitoredAddresses.isEmpty()) {
- StringBuilder builder = new StringBuilder();
- for (InsteonAddress insteonAddress : monitoredAddresses) {
- if (builder.length() == 0) {
- builder = new StringBuilder("The individual device(s) ");
- } else {
- builder.append(", ");
- }
- builder.append(insteonAddress);
- }
- console.println(builder.append(" are monitored").toString());
- } else if (monitorAllDevices) {
- console.println("All devices are monitored.");
- } else {
- console.println("Not mointoring any devices.");
+ @Override
+ public void execute(String[] args, Console console) {
+ if (legacyCommandExtension.isAvailable()) {
+ legacyCommandExtension.execute(args, console);
+ return;
}
- }
- private void startMonitoring(Console console, String addr) {
- if ("all".equalsIgnoreCase(addr)) {
- if (!monitorAllDevices) {
- monitorAllDevices = true;
- monitoredAddresses.clear();
- console.println("Started monitoring all devices.");
- } else {
- console.println("Already monitoring all devices.");
- }
- } else {
- try {
- if (monitorAllDevices) {
- console.println("Already monitoring all devices.");
- } else if (monitoredAddresses.add(new InsteonAddress(addr))) {
- console.println("Started monitoring the device " + addr + ".");
- } else {
- console.println("Already monitoring the device " + addr + ".");
- }
- } catch (IllegalArgumentException e) {
- console.println("Invalid device address" + addr + ".");
- return;
- }
+ InsteonBridgeHandler handler = getBridgeHandler();
+ if (handler == null) {
+ console.println("No Insteon bridge configured or enabled.");
+ return;
}
- if (!monitoring) {
- getInsteonBinding().getDriver().addMsgListener(this);
-
- this.console = console;
- monitoring = true;
+ if (handler.getModem() == null) {
+ console.println("Insteon bridge " + handler.getThing().getUID() + " not initialized yet.");
+ return;
}
- }
- private void stopMonitoring(Console console, String addr) {
- if (!monitoring) {
- console.println("Not mointoring any devices.");
+ if (args.length == 0) {
+ printUsage(console);
return;
}
- if ("all".equalsIgnoreCase(addr)) {
- if (monitorAllDevices) {
- monitorAllDevices = false;
- console.println("Stopped monitoring all devices.");
- } else {
- console.println("Not monitoring all devices.");
- }
+ InsteonCommand command = subCommands.get(args[0]);
+ if (command != null) {
+ command.execute(Arrays.copyOfRange(args, 1, args.length), console);
} else {
- try {
- if (monitorAllDevices) {
- console.println("Not monitoring individual devices.");
- } else if (monitoredAddresses.remove(new InsteonAddress(addr))) {
- console.println("Stopped monitoring the device " + addr + ".");
- } else {
- console.println("Not monitoring the device " + addr + ".");
- return;
- }
- } catch (IllegalArgumentException e) {
- console.println("Invalid address device address " + addr + ".");
- return;
- }
- }
-
- if (!monitorAllDevices && monitoredAddresses.isEmpty()) {
- getInsteonBinding().getDriver().removeListener(this);
- this.console = null;
- monitoring = false;
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
}
}
- private void sendMessage(Console console, MessageType messageType, String[] args) {
- InsteonDevice device = new InsteonDevice();
- device.setDriver(getInsteonBinding().getDriver());
-
- try {
- device.setAddress(new InsteonAddress(args[1]));
- } catch (IllegalArgumentException e) {
- console.println("Invalid device address" + args[1] + ".");
- return;
- }
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ InsteonBridgeHandler handler = getBridgeHandler();
+ if (!legacyCommandExtension.isAvailable() && handler != null && handler.getModem() != null) {
+ if (cursorArgumentIndex == 0) {
+ return new StringsCompleter(subCommands.keySet(), false).complete(args, cursorArgumentIndex,
+ cursorPosition, candidates);
+ }
- StringBuilder builder = new StringBuilder();
- for (int i = 2; i < args.length; i++) {
- if (!args[i].matches("\\p{XDigit}{1,2}")) {
- if (builder.length() > 0) {
- builder.append(", ");
- }
- builder.append(args[i]);
+ ConsoleCommandCompleter completer = subCommands.get(args[0]);
+ if (completer != null) {
+ return completer.complete(Arrays.copyOfRange(args, 1, args.length), cursorArgumentIndex - 1,
+ cursorPosition, candidates);
}
}
- if (builder.length() != 0) {
- builder.append(" is not a valid hexadecimal byte.");
- console.print(builder.toString());
- return;
+ return false;
+ }
+
+ public @Nullable InsteonBridgeHandler getBridgeHandler() {
+ InsteonBridgeHandler handler = this.handler;
+ if (handler == null || !handler.getThing().isEnabled()) {
+ return getBridgeHandlers().findFirst().orElse(null);
}
+ return handler;
+ }
- try {
- byte flags = (byte) Utils.fromHexString(args[2]);
- byte cmd1 = (byte) Utils.fromHexString(args[3]);
- byte cmd2 = (byte) Utils.fromHexString(args[4]);
- Msg msg;
- if (messageType == MessageType.STANDARD) {
- msg = device.makeStandardMessage(flags, cmd1, cmd2);
- } else {
- byte[] data = new byte[args.length - 5];
- for (int i = 0; i + 5 < args.length; i++) {
- data[i] = (byte) Utils.fromHexString(args[i + 5]);
- }
+ public Stream getBridgeHandlers() {
+ return thingRegistry.getAll().stream().filter(Thing::isEnabled).map(Thing::getHandler)
+ .filter(InsteonBridgeHandler.class::isInstance).map(InsteonBridgeHandler.class::cast);
+ }
- if (messageType == MessageType.EXTENDED) {
- msg = device.makeExtendedMessage(flags, cmd1, cmd2, data);
- } else {
- msg = device.makeExtendedMessageCRC2(flags, cmd1, cmd2, data);
- }
- }
- device.enqueueMessage(msg, new DeviceFeature(device, "console"));
- } catch (FieldException | InvalidMessageTypeException e) {
- console.println("Error while trying to create message.");
- }
+ public void setBridgeHandler(InsteonBridgeHandler handler) {
+ this.handler = handler;
}
- private InsteonBinding getInsteonBinding() {
- InsteonNetworkHandler handler = this.handler;
- if (handler == null) {
- throw new IllegalArgumentException("No Insteon network bridge configured.");
+ private @Nullable InsteonCommand instantiateCommand(Class extends InsteonCommand> clazz) {
+ try {
+ return clazz.getDeclaredConstructor(InsteonCommandExtension.class).newInstance(this);
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
+ | InvocationTargetException e) {
+ return null;
}
-
- return handler.getInsteonBinding();
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonLegacyCommandExtension.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonLegacyCommandExtension.java
new file mode 100644
index 0000000000000..0e00faba5186d
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/InsteonLegacyCommandExtension.java
@@ -0,0 +1,352 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.InsteonBindingConstants;
+import org.openhab.binding.insteon.internal.InsteonLegacyBinding;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.LegacyDevice;
+import org.openhab.binding.insteon.internal.device.LegacyDeviceFeature;
+import org.openhab.binding.insteon.internal.handler.InsteonLegacyNetworkHandler;
+import org.openhab.binding.insteon.internal.transport.LegacyPortListener;
+import org.openhab.binding.insteon.internal.transport.message.FieldException;
+import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingRegistry;
+
+/**
+ *
+ * The {@link InsteonLegacyCommandExtension} is responsible for handling legacy console commands
+ *
+ * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
+ */
+@NonNullByDefault
+public class InsteonLegacyCommandExtension extends AbstractConsoleCommandExtension implements LegacyPortListener {
+ private static final String DISPLAY_DEVICES = "display_devices";
+ private static final String DISPLAY_CHANNELS = "display_channels";
+ private static final String DISPLAY_LOCAL_DATABASE = "display_local_database";
+ private static final String DISPLAY_MONITORED = "display_monitored";
+ private static final String START_MONITORING = "start_monitoring";
+ private static final String STOP_MONITORING = "stop_monitoring";
+ private static final String SEND_STANDARD_MESSAGE = "send_standard_message";
+ private static final String SEND_EXTENDED_MESSAGE = "send_extended_message";
+ private static final String SEND_EXTENDED_MESSAGE_2 = "send_extended_message_2";
+
+ private enum MessageType {
+ STANDARD,
+ EXTENDED,
+ EXTENDED_2
+ }
+
+ private final ThingRegistry thingRegistry;
+
+ @Nullable
+ private Console console;
+ private boolean monitoring = false;
+ private boolean monitorAllDevices = false;
+ private Set monitoredAddresses = new HashSet<>();
+
+ public InsteonLegacyCommandExtension(final ThingRegistry thingRegistry) {
+ super(InsteonBindingConstants.BINDING_ID, "Interact with the Insteon integration.");
+ this.thingRegistry = thingRegistry;
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length > 0) {
+ InsteonLegacyNetworkHandler handler = getLegacyNetworkHandler();
+ if (handler == null) {
+ console.println("No Insteon legacy network bridge configured.");
+ } else {
+ switch (args[0]) {
+ case DISPLAY_DEVICES:
+ if (args.length == 1) {
+ handler.displayDevices(console);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case DISPLAY_CHANNELS:
+ if (args.length == 1) {
+ handler.displayChannels(console);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case DISPLAY_LOCAL_DATABASE:
+ if (args.length == 1) {
+ handler.displayLocalDatabase(console);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case DISPLAY_MONITORED:
+ if (args.length == 1) {
+ displayMonitoredDevices(console);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case START_MONITORING:
+ if (args.length == 2) {
+ startMonitoring(console, args[1]);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case STOP_MONITORING:
+ if (args.length == 2) {
+ stopMonitoring(console, args[1]);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case SEND_STANDARD_MESSAGE:
+ if (args.length == 5) {
+ sendMessage(console, MessageType.STANDARD, args);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case SEND_EXTENDED_MESSAGE:
+ if (args.length >= 5 && args.length <= 18) {
+ sendMessage(console, MessageType.EXTENDED, args);
+ } else {
+ printUsage(console);
+ }
+ break;
+ case SEND_EXTENDED_MESSAGE_2:
+ if (args.length >= 5 && args.length <= 17) {
+ sendMessage(console, MessageType.EXTENDED_2, args);
+ } else {
+ printUsage(console);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+ } else {
+ printUsage(console);
+ }
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(
+ buildCommandUsage(DISPLAY_DEVICES,
+ "display legacy devices that are online, along with available channels"),
+ buildCommandUsage(DISPLAY_CHANNELS,
+ "display legacy channels that are linked, along with configuration information"),
+ buildCommandUsage(DISPLAY_LOCAL_DATABASE, "display Insteon PLM or hub database details"),
+ buildCommandUsage(DISPLAY_MONITORED, "display monitored device(s)"),
+ buildCommandUsage(START_MONITORING + " all|address",
+ "start displaying messages received from device(s)"),
+ buildCommandUsage(STOP_MONITORING + " all|address", "stop displaying messages received from device(s)"),
+ buildCommandUsage(SEND_STANDARD_MESSAGE + " address flags cmd1 cmd2",
+ "send standard message to a device"),
+ buildCommandUsage(SEND_EXTENDED_MESSAGE + " address flags cmd1 cmd2 [up to 13 bytes]",
+ "send extended message to a device"),
+ buildCommandUsage(SEND_EXTENDED_MESSAGE_2 + " address flags cmd1 cmd2 [up to 12 bytes]",
+ "send extended message with a two byte crc to a device"));
+ }
+
+ @Override
+ public void msg(Msg msg) {
+ try {
+ if (monitorAllDevices || monitoredAddresses.contains(msg.getInsteonAddress("fromAddress"))) {
+ String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
+ Console console = this.console;
+ if (console != null) {
+ console.println(date + " " + msg.toString());
+ }
+ }
+ } catch (FieldException ignored) {
+ // ignore message with no address field
+ }
+ }
+
+ public boolean isAvailable() {
+ return getLegacyNetworkHandler() != null;
+ }
+
+ private void displayMonitoredDevices(Console console) {
+ if (!monitoredAddresses.isEmpty()) {
+ StringBuilder builder = new StringBuilder();
+ for (InsteonAddress insteonAddress : monitoredAddresses) {
+ if (builder.length() == 0) {
+ builder = new StringBuilder("The individual device(s) ");
+ } else {
+ builder.append(", ");
+ }
+ builder.append(insteonAddress);
+ }
+ console.println(builder.append(" are monitored").toString());
+ } else if (monitorAllDevices) {
+ console.println("All devices are monitored.");
+ } else {
+ console.println("Not monitoring any devices.");
+ }
+ }
+
+ private void startMonitoring(Console console, String addr) {
+ if ("all".equalsIgnoreCase(addr)) {
+ if (!monitorAllDevices) {
+ monitorAllDevices = true;
+ monitoredAddresses.clear();
+ console.println("Started monitoring all devices.");
+ } else {
+ console.println("Already monitoring all devices.");
+ }
+ } else {
+ try {
+ if (monitorAllDevices) {
+ console.println("Already monitoring all devices.");
+ } else if (monitoredAddresses.add(new InsteonAddress(addr))) {
+ console.println("Started monitoring the device " + addr + ".");
+ } else {
+ console.println("Already monitoring the device " + addr + ".");
+ }
+ } catch (IllegalArgumentException e) {
+ console.println("Invalid device address" + addr + ".");
+ return;
+ }
+ }
+
+ if (!monitoring) {
+ getInsteonBinding().getDriver().addPortListener(this);
+
+ this.console = console;
+ monitoring = true;
+ }
+ }
+
+ private void stopMonitoring(Console console, String addr) {
+ if (!monitoring) {
+ console.println("Not monitoring any devices.");
+ return;
+ }
+
+ if ("all".equalsIgnoreCase(addr)) {
+ if (monitorAllDevices) {
+ monitorAllDevices = false;
+ console.println("Stopped monitoring all devices.");
+ } else {
+ console.println("Not monitoring all devices.");
+ }
+ } else {
+ try {
+ if (monitorAllDevices) {
+ console.println("Not monitoring individual devices.");
+ } else if (monitoredAddresses.remove(new InsteonAddress(addr))) {
+ console.println("Stopped monitoring the device " + addr + ".");
+ } else {
+ console.println("Not monitoring the device " + addr + ".");
+ return;
+ }
+ } catch (IllegalArgumentException e) {
+ console.println("Invalid address device address " + addr + ".");
+ return;
+ }
+ }
+
+ if (!monitorAllDevices && monitoredAddresses.isEmpty()) {
+ getInsteonBinding().getDriver().removePortListener(this);
+ this.console = null;
+ monitoring = false;
+ }
+ }
+
+ private void sendMessage(Console console, MessageType messageType, String[] args) {
+ LegacyDevice device = new LegacyDevice();
+ device.setDriver(getInsteonBinding().getDriver());
+
+ try {
+ device.setAddress(new InsteonAddress(args[1]));
+ } catch (IllegalArgumentException e) {
+ console.println("Invalid device address" + args[1] + ".");
+ return;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 2; i < args.length; i++) {
+ if (!args[i].matches("\\p{XDigit}{1,2}")) {
+ if (builder.length() > 0) {
+ builder.append(", ");
+ }
+ builder.append(args[i]);
+ }
+ }
+ if (builder.length() != 0) {
+ builder.append(" is not a valid hexadecimal byte.");
+ console.print(builder.toString());
+ return;
+ }
+
+ try {
+ InsteonAddress address = (InsteonAddress) device.getAddress();
+ byte flags = (byte) HexUtils.toInteger(args[2]);
+ byte cmd1 = (byte) HexUtils.toInteger(args[3]);
+ byte cmd2 = (byte) HexUtils.toInteger(args[4]);
+ Msg msg;
+ if (messageType == MessageType.STANDARD) {
+ msg = Msg.makeStandardMessage(address, flags, cmd1, cmd2);
+ } else {
+ byte[] data = new byte[args.length - 5];
+ for (int i = 0; i + 5 < args.length; i++) {
+ data[i] = (byte) HexUtils.toInteger(args[i + 5]);
+ }
+
+ msg = Msg.makeExtendedMessage(address, flags, cmd1, cmd2, data, false);
+ if (messageType == MessageType.EXTENDED) {
+ msg.setCRC();
+ } else {
+ msg.setCRC2();
+ }
+ }
+ device.enqueueMessage(msg, new LegacyDeviceFeature(device, "console"));
+ } catch (FieldException | InvalidMessageTypeException e) {
+ console.println("Error while trying to create message.");
+ }
+ }
+
+ private @Nullable InsteonLegacyNetworkHandler getLegacyNetworkHandler() {
+ return thingRegistry.getAll().stream().filter(Thing::isEnabled).map(Thing::getHandler)
+ .filter(InsteonLegacyNetworkHandler.class::isInstance).map(InsteonLegacyNetworkHandler.class::cast)
+ .findFirst().orElse(null);
+ }
+
+ private InsteonLegacyBinding getInsteonBinding() {
+ InsteonLegacyNetworkHandler handler = getLegacyNetworkHandler();
+ if (handler == null) {
+ throw new IllegalArgumentException("No Insteon legacy network bridge configured.");
+ }
+
+ return handler.getInsteonBinding();
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ModemCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ModemCommand.java
new file mode 100644
index 0000000000000..089338bb07306
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/ModemCommand.java
@@ -0,0 +1,417 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.ProductData;
+import org.openhab.binding.insteon.internal.device.database.ModemDBEntry;
+import org.openhab.binding.insteon.internal.device.database.ModemDBRecord;
+import org.openhab.binding.insteon.internal.handler.InsteonBridgeHandler;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.StringsCompleter;
+
+/**
+ *
+ * The {@link ModemCommand} represents an Insteon console modem command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class ModemCommand extends InsteonCommand {
+ private static final String NAME = "modem";
+ private static final String DESCRIPTION = "Insteon modem commands";
+
+ private static final String LIST_ALL = "listAll";
+ private static final String LIST_DATABASE = "listDatabase";
+ private static final String RELOAD_DATABASE = "reloadDatabase";
+ private static final String ADD_DATABASE_CONTROLLER = "addDatabaseController";
+ private static final String ADD_DATABASE_RESPONDER = "addDatabaseResponder";
+ private static final String DELETE_DATABASE_RECORD = "deleteDatabaseRecord";
+ private static final String APPLY_DATABASE_CHANGES = "applyDatabaseChanges";
+ private static final String CLEAR_DATABASE_CHANGES = "clearDatabaseChanges";
+ private static final String ADD_DEVICE = "addDevice";
+ private static final String REMOVE_DEVICE = "removeDevice";
+ private static final String SWITCH = "switch";
+
+ private static final List SUBCMDS = List.of(LIST_ALL, LIST_DATABASE, RELOAD_DATABASE,
+ ADD_DATABASE_CONTROLLER, ADD_DATABASE_RESPONDER, DELETE_DATABASE_RECORD, APPLY_DATABASE_CHANGES,
+ CLEAR_DATABASE_CHANGES, ADD_DEVICE, REMOVE_DEVICE, SWITCH);
+
+ private static final String CONFIRM_OPTION = "--confirm";
+ private static final String FORCE_OPTION = "--force";
+ private static final String RECORDS_OPTION = "--records";
+
+ public ModemCommand(InsteonCommandExtension commandExtension) {
+ super(NAME, DESCRIPTION, commandExtension);
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(
+ buildCommandUsage(LIST_ALL, "list configured Insteon modem bridges with related channels and status"),
+ buildCommandUsage(LIST_DATABASE + " [" + RECORDS_OPTION + "]",
+ "list all-link database summary or records and pending changes for the Insteon modem"),
+ buildCommandUsage(RELOAD_DATABASE, "reload all-link database from the Insteon modem"),
+ buildCommandUsage(ADD_DATABASE_CONTROLLER + " [ ]",
+ "add a controller record to all-link database for the Insteon modem"),
+ buildCommandUsage(ADD_DATABASE_RESPONDER + " ",
+ "add a responder record to all-link database for the Insteon modem"),
+ buildCommandUsage(DELETE_DATABASE_RECORD + " ",
+ "delete a controller/responder record from all-link database for the Insteon modem"),
+ buildCommandUsage(APPLY_DATABASE_CHANGES + " " + CONFIRM_OPTION,
+ "apply all-link database pending changes for the Insteon modem"),
+ buildCommandUsage(CLEAR_DATABASE_CHANGES,
+ "clear all-link database pending changes for the Insteon modem"),
+ buildCommandUsage(ADD_DEVICE + " []",
+ "add an Insteon device to the modem, optionally providing its address"),
+ buildCommandUsage(REMOVE_DEVICE + " [" + FORCE_OPTION + "]",
+ "remove an Insteon device from the modem"),
+ buildCommandUsage(SWITCH + " ",
+ "switch Insteon modem bridge to use if more than one configured and enabled"));
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+
+ switch (args[0]) {
+ case LIST_ALL:
+ if (args.length == 1) {
+ listAll(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_DATABASE:
+ if (args.length == 1) {
+ listDatabaseSummary(console);
+ } else if (args.length == 2 && RECORDS_OPTION.equals(args[1])) {
+ listDatabaseRecords(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case RELOAD_DATABASE:
+ if (args.length == 1) {
+ reloadDatabase(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DATABASE_CONTROLLER:
+ if (args.length == 3 || args.length == 6) {
+ addDatabaseRecord(console, args, true);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DATABASE_RESPONDER:
+ if (args.length == 3) {
+ addDatabaseRecord(console, args, false);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case DELETE_DATABASE_RECORD:
+ if (args.length == 3) {
+ deleteDatabaseRecord(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case APPLY_DATABASE_CHANGES:
+ if (args.length == 1 || args.length == 2 && CONFIRM_OPTION.equals(args[1])) {
+ applyDatabaseChanges(console, args.length == 2);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case CLEAR_DATABASE_CHANGES:
+ if (args.length == 1) {
+ clearDatabaseChanges(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DEVICE:
+ if (args.length >= 1 && args.length <= 2) {
+ addDevice(console, args.length == 1 ? null : args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case REMOVE_DEVICE:
+ if (args.length == 2 || args.length == 3 && FORCE_OPTION.equals(args[2])) {
+ removeDevice(console, args[1], args.length == 3);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case SWITCH:
+ if (args.length == 2) {
+ switchModem(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ List strings = List.of();
+ if (cursorArgumentIndex == 0) {
+ strings = SUBCMDS;
+ } else if (cursorArgumentIndex == 1) {
+ switch (args[0]) {
+ case LIST_DATABASE:
+ strings = List.of(RECORDS_OPTION);
+ break;
+ case ADD_DATABASE_CONTROLLER:
+ case ADD_DATABASE_RESPONDER:
+ case REMOVE_DEVICE:
+ strings = getModem().getDB().getDevices().stream().map(InsteonAddress::toString).toList();
+ break;
+ case DELETE_DATABASE_RECORD:
+ strings = getModem().getDB().getRecords().stream().map(record -> record.getAddress().toString())
+ .distinct().toList();
+ break;
+ case SWITCH:
+ strings = getBridgeHandlers().map(InsteonBridgeHandler::getThingId).toList();
+ break;
+ }
+ } else if (cursorArgumentIndex == 2) {
+ InsteonAddress address = InsteonAddress.isValid(args[1]) ? new InsteonAddress(args[1]) : null;
+ switch (args[0]) {
+ case DELETE_DATABASE_RECORD:
+ if (address != null) {
+ strings = getModem().getDB().getRecords(address).stream()
+ .map(record -> HexUtils.getHexString(record.getGroup())).distinct().toList();
+ }
+ break;
+ case REMOVE_DEVICE:
+ strings = List.of(FORCE_OPTION);
+ break;
+ }
+ }
+
+ return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+
+ private void listAll(Console console) {
+ Map bridges = getBridgeHandlers()
+ .collect(Collectors.toMap(InsteonBridgeHandler::getThingId, InsteonBridgeHandler::getThingInfo));
+ if (bridges.isEmpty()) {
+ console.println("No modem bridge configured or enabled!");
+ } else {
+ console.println("There are " + bridges.size() + " modem bridges configured:");
+ print(console, bridges);
+ }
+ }
+
+ private void listDatabaseSummary(Console console) {
+ InsteonAddress address = getModem().getAddress();
+ Map entries = getModem().getDB().getEntries().stream()
+ .collect(Collectors.toMap(ModemDBEntry::getId, ModemDBEntry::toString));
+ if (InsteonAddress.UNKNOWN.equals(address)) {
+ console.println("No modem found!");
+ } else if (entries.isEmpty()) {
+ console.println("The all-link database for modem " + address + " is empty");
+ } else {
+ console.println("The all-link database for modem " + address + " contains " + entries.size() + " devices:");
+ print(console, entries);
+ }
+ }
+
+ private void listDatabaseRecords(Console console) {
+ InsteonAddress address = getModem().getAddress();
+ List records = getModem().getDB().getRecords().stream().map(ModemDBRecord::toString).toList();
+ if (InsteonAddress.UNKNOWN.equals(address)) {
+ console.println("No modem found!");
+ } else if (records.isEmpty()) {
+ console.println("The all-link database for modem " + address + " is empty");
+ } else {
+ console.println("The all-link database for modem " + address + " contains " + records.size() + " records:");
+ print(console, records);
+ listDatabaseChanges(console);
+ }
+ }
+
+ private void listDatabaseChanges(Console console) {
+ InsteonAddress address = getModem().getAddress();
+ List changes = getModem().getDB().getChanges().stream().map(String::valueOf).toList();
+ if (InsteonAddress.UNKNOWN.equals(address)) {
+ console.println("No modem found!");
+ } else if (!changes.isEmpty()) {
+ console.println(
+ "The all-link database for modem " + address + " has " + changes.size() + " pending changes:");
+ print(console, changes);
+ }
+ }
+
+ private void reloadDatabase(Console console) {
+ InsteonAddress address = getModem().getAddress();
+ InsteonBridgeHandler handler = getBridgeHandler();
+ if (InsteonAddress.UNKNOWN.equals(address)) {
+ console.println("No modem found!");
+ } else {
+ console.println("Reloading all-link database for modem " + address + ".");
+ getModem().getDB().clear();
+ handler.reset(0);
+ }
+ }
+
+ private void addDatabaseRecord(Console console, String[] args, boolean isController) {
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (!InsteonAddress.isValid(args[1])) {
+ console.println("Invalid record address argument: " + args[1]);
+ } else if (!HexUtils.isValidHexString(args[2])) {
+ console.println("Invalid record group hex argument: " + args[2]);
+ } else if (isController && args.length == 6 && !HexUtils.isValidHexStringArray(args, 3, args.length)) {
+ console.println("Invalid product data hex argument(s).");
+ } else if (isController && args.length == 3
+ && !getModem().getDB().hasProductData(new InsteonAddress(args[1]))) {
+ console.println("No product data available for " + args[1] + ".");
+ } else {
+ InsteonAddress address = new InsteonAddress(args[1]);
+ int group = HexUtils.toInteger(args[2]);
+ byte data[] = new byte[3];
+ if (isController) {
+ ProductData productData = getModem().getDB().getProductData(address);
+ if (args.length == 6) {
+ data = HexUtils.toByteArray(args, 3, args.length);
+ } else if (args.length == 3 && productData != null) {
+ data = productData.getRecordData();
+ }
+ }
+
+ ModemDBRecord record = getModem().getDB().getRecord(address, group, isController);
+ if (record == null) {
+ getModem().getDB().markRecordForAdd(address, group, isController, data);
+
+ } else {
+ getModem().getDB().markRecordForModify(record, data);
+ }
+ console.println("Added a pending change to " + (record == null ? "add" : "modify") + " modem database "
+ + (isController ? "controller" : "responder") + " record with address " + address + " and group "
+ + group + ".");
+ }
+ }
+
+ private void deleteDatabaseRecord(Console console, String[] args) {
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (!InsteonAddress.isValid(args[1])) {
+ console.println("Invalid record address argument: " + args[1]);
+ } else if (!HexUtils.isValidHexString(args[2])) {
+ console.println("Invalid record group hex argument: " + args[2]);
+ } else {
+ InsteonAddress address = new InsteonAddress(args[1]);
+ int group = HexUtils.toInteger(args[2]);
+
+ ModemDBRecord record = getModem().getDB().getRecord(address, group);
+ if (record == null) {
+ console.println(
+ "No modem database record with address " + address + " and group " + group + " to delete.");
+ } else {
+ getModem().getDB().markRecordForDelete(record);
+ console.println("Added a pending change to delete modem database "
+ + (record.isController() ? "controller" : "responder") + " record with address " + address
+ + " and group " + group + ".");
+ }
+ }
+ }
+
+ private void applyDatabaseChanges(Console console, boolean isConfirmed) {
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (getModem().getDB().getChanges().isEmpty()) {
+ console.println("The modem database has no pending changes.");
+ } else if (!isConfirmed) {
+ listDatabaseChanges(console);
+ console.println("Please run the same command with " + CONFIRM_OPTION
+ + " option to have these changes written to the modem database.");
+ } else {
+ int count = getModem().getDB().getChanges().size();
+ console.println("Applying " + count + " pending changes to the modem database...");
+ getModem().getDB().update();
+ }
+ }
+
+ private void clearDatabaseChanges(Console console) {
+ if (getModem().getDB().getChanges().isEmpty()) {
+ console.println("The modem database has no pending changes.");
+ } else {
+ int count = getModem().getDB().getChanges().size();
+ getModem().getDB().clearChanges();
+ console.println("Cleared " + count + " pending changes from the modem database.");
+ }
+ }
+
+ private void addDevice(Console console, @Nullable String address) {
+ if (address != null && !InsteonAddress.isValid(address)) {
+ console.println("The device address " + address + " is not valid.");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (getModem().getLinkManager().isRunning()) {
+ console.println("Another device is currently being added or removed.");
+ } else if (address == null) {
+ console.println("Adding device...");
+ console.println("Press the device SET button to link.");
+ getModem().getLinkManager().link(null);
+ } else {
+ console.println("Adding device " + address + "...");
+ getModem().getLinkManager().link(new InsteonAddress(address));
+ }
+ }
+
+ private void removeDevice(Console console, String address, boolean force) {
+ if (!InsteonAddress.isValid(address)) {
+ console.println("The device address " + address + " is not valid.");
+ } else if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ } else if (!getModem().getDB().hasEntry(new InsteonAddress(address))) {
+ console.println("The device " + address + " is not in modem database.");
+ } else if (getModem().getLinkManager().isRunning()) {
+ console.println("Another device is currently being added or removed.");
+ } else {
+ console.println("Removing device " + address + "...");
+ getModem().getLinkManager().unlink(new InsteonAddress(address), force);
+ }
+ }
+
+ private void switchModem(Console console, String thingId) {
+ InsteonBridgeHandler handler = getBridgeHandler(thingId);
+ if (handler == null) {
+ console.println("No Insteon bridge " + thingId + " configured or enabled.");
+ } else {
+ console.println("Using Insteon bridge " + handler.getThing().getUID());
+ setBridgeHandler(handler);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/SceneCommand.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/SceneCommand.java
new file mode 100644
index 0000000000000..c5c63934c1fb2
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/command/SceneCommand.java
@@ -0,0 +1,318 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.command;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.device.DeviceFeature;
+import org.openhab.binding.insteon.internal.device.InsteonAddress;
+import org.openhab.binding.insteon.internal.device.InsteonDevice;
+import org.openhab.binding.insteon.internal.device.InsteonScene;
+import org.openhab.binding.insteon.internal.device.OnLevel;
+import org.openhab.binding.insteon.internal.device.RampRate;
+import org.openhab.binding.insteon.internal.handler.InsteonDeviceHandler;
+import org.openhab.binding.insteon.internal.handler.InsteonSceneHandler;
+import org.openhab.core.io.console.Console;
+import org.openhab.core.io.console.StringsCompleter;
+
+/**
+ *
+ * The {@link SceneCommand} represents an Insteon console scene command
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class SceneCommand extends InsteonCommand {
+ private static final String NAME = "scene";
+ private static final String DESCRIPTION = "Insteon scene commands";
+
+ private static final String LIST_ALL = "listAll";
+ private static final String LIST_DETAILS = "listDetails";
+ private static final String ADD_DEVICE = "addDevice";
+ private static final String REMOVE_DEVICE = "removeDevice";
+
+ private static final List SUBCMDS = List.of(LIST_ALL, LIST_DETAILS, ADD_DEVICE, REMOVE_DEVICE);
+
+ private static final String NEW_OPTION = "--new";
+
+ public SceneCommand(InsteonCommandExtension commandExtension) {
+ super(NAME, DESCRIPTION, commandExtension);
+ }
+
+ @Override
+ public List getUsages() {
+ return List.of(buildCommandUsage(LIST_ALL, "list configured Insteon scenes with related channels and status"),
+ buildCommandUsage(LIST_DETAILS + " ", "list details for a configured Insteon scene"),
+ buildCommandUsage(ADD_DEVICE + " " + NEW_OPTION + "| []",
+ "add an Insteon device feature to a new or configured Insteon scene"),
+ buildCommandUsage(REMOVE_DEVICE + " ",
+ "remove an Insteon device feature from a configured Insteon scene"));
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length == 0) {
+ printUsage(console);
+ return;
+ }
+
+ switch (args[0]) {
+ case LIST_ALL:
+ if (args.length == 1) {
+ listAll(console);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case LIST_DETAILS:
+ if (args.length == 2) {
+ listDetails(console, args[1]);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case ADD_DEVICE:
+ if (args.length >= 5 && args.length <= 6) {
+ addDevice(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ case REMOVE_DEVICE:
+ if (args.length == 4) {
+ removeDevice(console, args);
+ } else {
+ printUsage(console, args[0]);
+ }
+ break;
+ default:
+ console.println("Unknown command '" + args[0] + "'");
+ printUsage(console);
+ break;
+ }
+ }
+
+ @Override
+ public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) {
+ List strings = List.of();
+ if (cursorArgumentIndex == 0) {
+ strings = SUBCMDS;
+ } else if (cursorArgumentIndex == 1) {
+ switch (args[0]) {
+ case LIST_DETAILS:
+ case REMOVE_DEVICE:
+ strings = getInsteonSceneHandlers().map(InsteonSceneHandler::getThingId).toList();
+ break;
+ case ADD_DEVICE:
+ strings = Stream.concat(Stream.of(NEW_OPTION),
+ getInsteonSceneHandlers().map(InsteonSceneHandler::getThingId)).toList();
+ break;
+ }
+ } else if (cursorArgumentIndex == 2) {
+ InsteonScene scene = getInsteonScene(args[1]);
+ switch (args[0]) {
+ case ADD_DEVICE:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && !device.getResponderFeatures().isEmpty();
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ case REMOVE_DEVICE:
+ strings = getInsteonDeviceHandlers().filter(handler -> {
+ InsteonDevice device = handler.getDevice();
+ return device != null && scene != null && scene.hasEntry(device.getAddress());
+ }).map(InsteonDeviceHandler::getThingId).toList();
+ break;
+ }
+ } else if (cursorArgumentIndex == 3) {
+ InsteonScene scene = getInsteonScene(args[1]);
+ InsteonDevice device = getInsteonDevice(args[2]);
+ switch (args[0]) {
+ case ADD_DEVICE:
+ if (device != null) {
+ strings = device.getResponderFeatures().stream().map(DeviceFeature::getName).toList();
+ }
+ break;
+ case REMOVE_DEVICE:
+ if (device != null && scene != null) {
+ strings = scene.getFeatures(device.getAddress()).stream().map(DeviceFeature::getName).toList();
+ }
+ break;
+ }
+
+ } else if (cursorArgumentIndex == 4) {
+ InsteonDevice device = getInsteonDevice(args[2]);
+ DeviceFeature feature = device != null ? device.getFeature(args[3]) : null;
+ switch (args[0]) {
+ case ADD_DEVICE:
+ if (feature != null) {
+ strings = OnLevel.getSupportedValues(feature.getType());
+ }
+ break;
+ }
+ } else if (cursorArgumentIndex == 5) {
+ InsteonDevice device = getInsteonDevice(args[2]);
+ DeviceFeature feature = device != null ? device.getFeature(args[3]) : null;
+ switch (args[0]) {
+ case ADD_DEVICE:
+ if (feature != null && RampRate.supportsFeatureType(feature.getType())) {
+ strings = Stream.of(RampRate.values()).map(String::valueOf).toList();
+ }
+ break;
+ }
+ }
+
+ return new StringsCompleter(strings, false).complete(args, cursorArgumentIndex, cursorPosition, candidates);
+ }
+
+ private void listAll(Console console) {
+ Map scenes = getInsteonSceneHandlers()
+ .collect(Collectors.toMap(InsteonSceneHandler::getThingId, InsteonSceneHandler::getThingInfo));
+ if (scenes.isEmpty()) {
+ console.println("No scene configured or enabled!");
+ } else {
+ console.println("There are " + scenes.size() + " scenes configured:");
+ print(console, scenes);
+ }
+ }
+
+ private void listDetails(Console console, String thingId) {
+ InsteonScene scene = getInsteonScene(thingId);
+ if (scene == null) {
+ console.println("The scene " + thingId + " is not configured or enabled!");
+ return;
+ }
+ List devices = scene.getDevices();
+ List entries = scene.getEntries().stream().map(String::valueOf).sorted().toList();
+ if (devices.isEmpty()) {
+ console.println("The scene " + scene.getGroup() + " has no associated device configured or enabled.");
+ } else {
+ console.println("The scene " + scene.getGroup() + " is currently " + scene.getState() + ". It controls "
+ + devices.size() + " devices:" + (scene.isComplete() ? "" : " (Partial)"));
+ print(console, entries);
+ }
+ }
+
+ private void addDevice(Console console, String[] args) {
+ InsteonScene scene;
+ if (NEW_OPTION.equals(args[1])) {
+ int group = getModem().getDB().getNextAvailableBroadcastGroup();
+ if (group != -1) {
+ scene = InsteonScene.makeScene(group, getModem());
+ } else {
+ console.println("Unable to create new scene, no broadcast group available!");
+ return;
+ }
+ } else {
+ scene = getInsteonScene(args[1]);
+ if (scene == null) {
+ console.println("The scene " + args[1] + " is not configured or enabled!");
+ return;
+ }
+ }
+ InsteonDevice device = getInsteonDevice(args[2]);
+ if (device == null) {
+ console.println("The device " + args[2] + " is not configured or enabled!");
+ return;
+ }
+ DeviceFeature feature = device.getFeature(args[3]);
+ if (feature == null) {
+ console.println("The device " + args[2] + " feature " + args[3] + " is not configured!");
+ return;
+ }
+ if (!feature.isResponderFeature()) {
+ console.println("The device " + args[2] + " feature " + args[3] + " is not a responder feature.");
+ return;
+ }
+ if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + args[2] + " is not loaded yet.");
+ return;
+ }
+ if (!device.getLinkDB().getChanges().isEmpty()) {
+ console.println("The link database for device " + args[2] + " has pending changes.");
+ return;
+ }
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ return;
+ }
+ if (!getModem().getDB().getChanges().isEmpty()) {
+ console.println("The modem database has pending changes.");
+ return;
+ }
+ int onLevel = OnLevel.getHexValue(args[4], feature.getType());
+ if (onLevel == -1) {
+ console.println("The feature " + args[3] + " onLevel " + args[4] + " is not valid.");
+ return;
+ }
+ RampRate rampRate = null;
+ if (RampRate.supportsFeatureType(feature.getType())) {
+ rampRate = args.length == 6 ? RampRate.fromString(args[5]) : RampRate.DEFAULT;
+ if (rampRate == null) {
+ console.println("The feature " + args[3] + " rampRate " + args[5] + " is not valid.");
+ return;
+ }
+ }
+
+ console.println("Adding device " + device.getAddress() + " feature " + feature.getName() + " to scene "
+ + scene.getGroup() + ".");
+ scene.addDeviceFeature(device, onLevel, rampRate, feature.getComponentId());
+ }
+
+ private void removeDevice(Console console, String[] args) {
+ InsteonScene scene = getInsteonScene(args[1]);
+ if (scene == null) {
+ console.println("The scene " + args[1] + " is not configured or enabled!");
+ return;
+ }
+ InsteonDevice device = getInsteonDevice(args[2]);
+ if (device == null) {
+ console.println("The device " + args[2] + " is not configured or enabled!");
+ return;
+ }
+ DeviceFeature feature = device.getFeature(args[3]);
+ if (feature == null) {
+ console.println("The device " + args[2] + " feature " + args[3] + " is not configured!");
+ return;
+ }
+ if (!device.getLinkDB().isComplete()) {
+ console.println("The link database for device " + args[2] + " is not loaded yet.");
+ return;
+ }
+ if (!device.getLinkDB().getChanges().isEmpty()) {
+ console.println("The link database for device " + args[2] + " has pending changes.");
+ return;
+ }
+ if (!getModem().getDB().isComplete()) {
+ console.println("The modem database is not loaded yet.");
+ return;
+ }
+ if (!getModem().getDB().getChanges().isEmpty()) {
+ console.println("The modem database has pending changes.");
+ return;
+ }
+ if (!scene.hasEntry(device.getAddress(), feature.getName())) {
+ console.println(
+ "The device " + args[2] + " feature " + args[3] + " is not associated to scene" + args[1] + ".");
+ return;
+ }
+
+ console.println("Removing device " + device.getAddress() + " feature " + feature.getName() + " from scene "
+ + scene.getGroup() + ".");
+ scene.removeDeviceFeature(device, feature.getComponentId());
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonBridgeConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonBridgeConfiguration.java
new file mode 100644
index 0000000000000..ccbb7fc4418a2
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonBridgeConfiguration.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link InsteonBridgeConfiguration} is the base configuration for insteon bridges.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public abstract class InsteonBridgeConfiguration {
+
+ private int devicePollIntervalInSeconds = 300;
+ private boolean deviceDiscoveryEnabled = true;
+ private boolean sceneDiscoveryEnabled = false;
+ private boolean deviceSyncEnabled = false;
+
+ public int getDevicePollInterval() {
+ return devicePollIntervalInSeconds * 1000; // in milliseconds
+ }
+
+ public boolean isDeviceDiscoveryEnabled() {
+ return deviceDiscoveryEnabled;
+ }
+
+ public boolean isSceneDiscoveryEnabled() {
+ return sceneDiscoveryEnabled;
+ }
+
+ public boolean isDeviceSyncEnabled() {
+ return deviceSyncEnabled;
+ }
+
+ public abstract String getId();
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " devicePollIntervalInSeconds=" + devicePollIntervalInSeconds;
+ s += " deviceDiscoveryEnabled=" + deviceDiscoveryEnabled;
+ s += " sceneDiscoveryEnabled=" + sceneDiscoveryEnabled;
+ s += " deviceSyncEnabled=" + deviceSyncEnabled;
+ return s;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonChannelConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonChannelConfiguration.java
index b0c73e97af197..07397bc564d8e 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonChannelConfiguration.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonChannelConfiguration.java
@@ -12,60 +12,62 @@
*/
package org.openhab.binding.insteon.internal.config;
-import java.util.Map;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.insteon.internal.device.InsteonAddress;
-import org.openhab.core.thing.ChannelUID;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.RampRate;
/**
*
- * This file contains config information needed for each channel
+ * The {@link InsteonChannelConfiguration} is the configuration for an insteon channel.
*
- * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Initial contribution
*/
@NonNullByDefault
public class InsteonChannelConfiguration {
- private final ChannelUID channelUID;
- private final String channelName;
- private final InsteonAddress address;
- private final String feature;
- private final String productKey;
- private final Map parameters;
-
- public InsteonChannelConfiguration(ChannelUID channelUID, String feature, InsteonAddress address, String productKey,
- Map parameters) {
- this.channelUID = channelUID;
- this.feature = feature;
- this.address = address;
- this.productKey = productKey;
- this.parameters = parameters;
-
- this.channelName = channelUID.getAsString();
- }
+ private int group = -1;
+ private int onLevel = -1;
+ private double rampRate = -1;
+ private boolean original = true;
- public ChannelUID getChannelUID() {
- return channelUID;
+ public int getGroup() {
+ return group;
}
- public String getChannelName() {
- return channelName;
+ public int getOnLevel() {
+ return onLevel;
}
- public InsteonAddress getAddress() {
- return address;
+ public @Nullable RampRate getRampRate() {
+ return rampRate != -1 ? RampRate.fromTime(rampRate) : null;
}
- public String getFeature() {
- return feature;
+ public boolean isOriginal() {
+ return original;
}
- public String getProductKey() {
- return productKey;
+ @Override
+ public String toString() {
+ String s = "";
+ if (group != -1) {
+ s += " group=" + group;
+ }
+ if (onLevel != -1) {
+ s += " onLevel=" + onLevel;
+ }
+ if (rampRate != -1) {
+ s += " rampRate=" + rampRate;
+ }
+ return s;
}
- public Map getParameters() {
- return parameters;
+ public static InsteonChannelConfiguration copyOf(InsteonChannelConfiguration original, int onLevel,
+ RampRate rampRate) {
+ InsteonChannelConfiguration config = new InsteonChannelConfiguration();
+ config.group = original.group;
+ config.onLevel = original.onLevel != -1 ? original.onLevel : onLevel;
+ config.rampRate = original.rampRate != -1 ? original.rampRate : rampRate.getTimeInSeconds();
+ config.original = false;
+ return config;
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonDeviceConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonDeviceConfiguration.java
index d1528907e4af1..cafd9efd1d988 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonDeviceConfiguration.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonDeviceConfiguration.java
@@ -13,34 +13,25 @@
package org.openhab.binding.insteon.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
/**
- * The {@link InsteonDeviceConfiguration} class contains fields mapping thing configuration parameters.
+ * The {@link InsteonDeviceConfiguration} is the configuration for an insteon device thing.
*
- * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Initial contribution
*/
@NonNullByDefault
public class InsteonDeviceConfiguration {
- // required parameter
private String address = "";
- // required parameter
- private String productKey = "";
-
- // optional parameter
- private @Nullable String deviceConfig;
-
public String getAddress() {
return address;
}
- public String getProductKey() {
- return productKey;
- }
-
- public @Nullable String getDeviceConfig() {
- return deviceConfig;
+ @Override
+ public String toString() {
+ String s = "";
+ s += " address=" + address;
+ return s;
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub1Configuration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub1Configuration.java
new file mode 100644
index 0000000000000..6a4a56d2fd5a9
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub1Configuration.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link InsteonHub1Configuration} is the configuration for an insteon hub 1 bridge.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class InsteonHub1Configuration extends InsteonBridgeConfiguration {
+
+ private String hostname = "";
+ private int port = 9761;
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ @Override
+ public String getId() {
+ return hostname + ":" + port;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " hostname=" + hostname;
+ s += " port=" + port;
+ s += super.toString();
+ return s;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ InsteonHub1Configuration other = (InsteonHub1Configuration) obj;
+ return hostname.equals(other.hostname) && port == other.port;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + hostname.hashCode();
+ result = prime * result + port;
+ return result;
+ }
+
+ public static InsteonHub1Configuration valueOf(String hostname, @Nullable Integer port) {
+ InsteonHub1Configuration config = new InsteonHub1Configuration();
+ config.hostname = hostname;
+ if (port != null) {
+ config.port = port;
+ }
+ return config;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub2Configuration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub2Configuration.java
new file mode 100644
index 0000000000000..ceb71077ad7b6
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonHub2Configuration.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link InsteonHub2Configuration} is the configuration for an insteon hub 2 bridge.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class InsteonHub2Configuration extends InsteonBridgeConfiguration {
+
+ private String hostname = "";
+ private int port = 25105;
+ private String username = "";
+ private String password = "";
+ private int hubPollIntervalInMilliseconds = 1000;
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public int getHubPollInterval() {
+ return hubPollIntervalInMilliseconds;
+ }
+
+ @Override
+ public String getId() {
+ return hostname + ":" + port;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " hostname=" + hostname;
+ s += " port=" + port;
+ s += " username=" + username;
+ s += " password=" + "*".repeat(password.length());
+ s += " hubPollIntervalInMilliseconds=" + hubPollIntervalInMilliseconds;
+ s += super.toString();
+ return s;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ InsteonHub2Configuration other = (InsteonHub2Configuration) obj;
+ return hostname.equals(other.hostname) && port == other.port;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + hostname.hashCode();
+ result = prime * result + port;
+ return result;
+ }
+
+ public static InsteonHub2Configuration valueOf(String hostname, @Nullable Integer port, String username,
+ String password, @Nullable Integer hubPollIntervalInMilliseconds) {
+ InsteonHub2Configuration config = new InsteonHub2Configuration();
+ config.hostname = hostname;
+ if (port != null) {
+ config.port = port;
+ }
+ config.username = username;
+ config.password = password;
+ if (hubPollIntervalInMilliseconds != null) {
+ config.hubPollIntervalInMilliseconds = hubPollIntervalInMilliseconds;
+ }
+ return config;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyChannelConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyChannelConfiguration.java
new file mode 100644
index 0000000000000..438c285f7eb3b
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyChannelConfiguration.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.DeviceAddress;
+import org.openhab.core.thing.ChannelUID;
+
+/**
+ * This file contains config information needed for each channel
+ *
+ * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
+ */
+@NonNullByDefault
+public class InsteonLegacyChannelConfiguration {
+
+ private final ChannelUID channelUID;
+ private final String channelName;
+ private final DeviceAddress address;
+ private final String feature;
+ private final String productKey;
+ private final Map parameters;
+
+ public InsteonLegacyChannelConfiguration(ChannelUID channelUID, String feature, DeviceAddress address,
+ String productKey, Map parameters) {
+ this.channelUID = channelUID;
+ this.feature = feature;
+ this.address = address;
+ this.productKey = productKey;
+ this.parameters = parameters;
+
+ this.channelName = channelUID.getAsString();
+ }
+
+ public ChannelUID getChannelUID() {
+ return channelUID;
+ }
+
+ public String getChannelName() {
+ return channelName;
+ }
+
+ public DeviceAddress getAddress() {
+ return address;
+ }
+
+ public String getFeature() {
+ return feature;
+ }
+
+ public String getProductKey() {
+ return productKey;
+ }
+
+ public Map getParameters() {
+ return parameters;
+ }
+
+ public @Nullable String getParameter(String key) {
+ return parameters.get(key);
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyDeviceConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyDeviceConfiguration.java
new file mode 100644
index 0000000000000..b8deb23b9cb59
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyDeviceConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link InsteonLegacyDeviceConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Rob Nielsen - Initial contribution
+ */
+@NonNullByDefault
+public class InsteonLegacyDeviceConfiguration {
+
+ private String address = "";
+ private String productKey = "";
+ private @Nullable String deviceConfig;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getProductKey() {
+ return productKey;
+ }
+
+ public @Nullable String getDeviceConfig() {
+ return deviceConfig;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyNetworkConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyNetworkConfiguration.java
new file mode 100644
index 0000000000000..96f03d525bd56
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonLegacyNetworkConfiguration.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link InsteonLegacyNetworkConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Rob Nielsen - Initial contribution
+ * @author Jeremy Setton - Rewrite insteon binding
+ */
+@NonNullByDefault
+public class InsteonLegacyNetworkConfiguration {
+ private static final Pattern HUB1_PORT_PATTERN = Pattern
+ .compile("/(?:hub|tcp)/(?[^:]+)(?::(?\\d+))?");
+ private static final Pattern HUB2_PORT_PATTERN = Pattern.compile(
+ "/hub2/(?[^:]+):(?[^@]+)@(?[^:,]+)(?::(?\\d+))?(?:,poll_time=(?\\d+))?");
+ private static final Pattern PLM_PORT_PATTERN = Pattern
+ .compile("(?[^,]+)(?:,baudRate=(?\\d+))?");
+
+ private String port = "";
+ private @Nullable Integer devicePollIntervalSeconds;
+ private @Nullable String additionalDevices;
+ private @Nullable String additionalFeatures;
+
+ public String getPort() {
+ return port;
+ }
+
+ public String getRedactedPort() {
+ return port.startsWith("/hub2/") ? port.replaceAll(":\\w+@", ":******@") : port;
+ }
+
+ public @Nullable Integer getDevicePollIntervalSeconds() {
+ return devicePollIntervalSeconds;
+ }
+
+ public @Nullable String getAdditionalDevices() {
+ return additionalDevices;
+ }
+
+ public @Nullable String getAdditionalFeatures() {
+ return additionalFeatures;
+ }
+
+ public boolean isParsable() {
+ try {
+ parse();
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ public InsteonBridgeConfiguration parse() {
+ Matcher hub1PortMatcher = HUB1_PORT_PATTERN.matcher(port);
+ if (hub1PortMatcher.matches()) {
+ return getHub1Config(hub1PortMatcher);
+ }
+ Matcher hub2PortMatcher = HUB2_PORT_PATTERN.matcher(port);
+ if (hub2PortMatcher.matches()) {
+ return getHub2Config(hub2PortMatcher);
+ }
+ Matcher plmPortMatcher = PLM_PORT_PATTERN.matcher(port);
+ if (plmPortMatcher.matches()) {
+ return getPLMConfig(plmPortMatcher);
+ }
+ throw new IllegalArgumentException("unable to parse bridge port parameter");
+ }
+
+ private InsteonHub1Configuration getHub1Config(Matcher matcher) {
+ String hostname = matcher.group("hostname");
+ Integer port = Optional.ofNullable(matcher.group("port")).map(Integer::parseInt).orElse(null);
+ return InsteonHub1Configuration.valueOf(hostname, port);
+ }
+
+ private InsteonHub2Configuration getHub2Config(Matcher matcher) {
+ String hostname = matcher.group("hostname");
+ Integer port = Optional.ofNullable(matcher.group("port")).map(Integer::parseInt).orElse(null);
+ String username = matcher.group("username");
+ String password = matcher.group("password");
+ Integer pollInterval = Optional.ofNullable(matcher.group("pollInterval")).map(Integer::parseInt).orElse(null);
+ return InsteonHub2Configuration.valueOf(hostname, port, username, password, pollInterval);
+ }
+
+ private InsteonPLMConfiguration getPLMConfig(Matcher matcher) {
+ String serialPort = matcher.group("serialPort");
+ Integer baudRate = Optional.ofNullable(matcher.group("baudRate")).map(Integer::parseInt).orElse(null);
+ return InsteonPLMConfiguration.valueOf(serialPort, baudRate);
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonNetworkConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonNetworkConfiguration.java
deleted file mode 100644
index 5d24fd7553707..0000000000000
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonNetworkConfiguration.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.insteon.internal.config;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The {@link InsteonNetworkConfiguration} class contains fields mapping thing configuration parameters.
- *
- * @author Rob Nielsen - Initial contribution
- */
-@NonNullByDefault
-public class InsteonNetworkConfiguration {
-
- // required parameter
- private String port = "";
-
- private @Nullable Integer devicePollIntervalSeconds;
-
- private @Nullable String additionalDevices;
-
- private @Nullable String additionalFeatures;
-
- public String getPort() {
- return port;
- }
-
- public @Nullable Integer getDevicePollIntervalSeconds() {
- return devicePollIntervalSeconds;
- }
-
- public @Nullable String getAdditionalDevices() {
- return additionalDevices;
- }
-
- public @Nullable String getAdditionalFeatures() {
- return additionalFeatures;
- }
-}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonPLMConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonPLMConfiguration.java
new file mode 100644
index 0000000000000..497e3db4917a0
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonPLMConfiguration.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link InsteonPLMConfiguration} is the configuration for an insteon plm bridge.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class InsteonPLMConfiguration extends InsteonBridgeConfiguration {
+
+ private String serialPort = "";
+ private int baudRate = 19200;
+
+ public String getSerialPort() {
+ return serialPort;
+ }
+
+ public int getBaudRate() {
+ return baudRate;
+ }
+
+ @Override
+ public String getId() {
+ return serialPort;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " serialPort=" + serialPort;
+ s += " baudRate=" + baudRate;
+ s += super.toString();
+ return s;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ InsteonPLMConfiguration other = (InsteonPLMConfiguration) obj;
+ return serialPort.equals(other.serialPort);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + serialPort.hashCode();
+ return result;
+ }
+
+ public static InsteonPLMConfiguration valueOf(String serialPort, @Nullable Integer baudRate) {
+ InsteonPLMConfiguration config = new InsteonPLMConfiguration();
+ config.serialPort = serialPort;
+ if (baudRate != null) {
+ config.baudRate = baudRate;
+ }
+ return config;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonSceneConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonSceneConfiguration.java
new file mode 100644
index 0000000000000..d3e6c82ffee0f
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/InsteonSceneConfiguration.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link InsteonSceneConfiguration} is the configuration for an insteon scene thing.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class InsteonSceneConfiguration {
+
+ private int group = -1;
+
+ public int getGroup() {
+ return group;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " group=" + group;
+ return s;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/X10DeviceConfiguration.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/X10DeviceConfiguration.java
new file mode 100644
index 0000000000000..9211179174a16
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/config/X10DeviceConfiguration.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link X10DeviceConfiguration} is the configuration for an x10 device thing.
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class X10DeviceConfiguration {
+
+ private String houseCode = "";
+ private int unitCode = 0;
+ private String deviceType = "";
+
+ public String getHouseCode() {
+ return houseCode;
+ }
+
+ public int getUnitCode() {
+ return unitCode;
+ }
+
+ public String getAddress() {
+ return houseCode + "." + unitCode;
+ }
+
+ public String getDeviceType() {
+ return deviceType;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ s += " houseCode=" + houseCode;
+ s += " unitCode=" + unitCode;
+ s += " deviceType=" + deviceType;
+ return s;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/BaseDevice.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/BaseDevice.java
new file mode 100644
index 0000000000000..2f7af2bed3253
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/BaseDevice.java
@@ -0,0 +1,645 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.function.Predicate;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.DeviceFeature.QueryStatus;
+import org.openhab.binding.insteon.internal.device.DeviceType.FeatureEntry;
+import org.openhab.binding.insteon.internal.handler.InsteonThingHandler;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BaseDevice} represents a base device
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BaseDevice<@NonNull T extends DeviceAddress, @NonNull S extends InsteonThingHandler>
+ implements Device {
+ private static final int DIRECT_ACK_TIMEOUT = 6000; // in milliseconds
+ private static final int REQUEST_QUEUE_TIMEOUT = 30000; // in milliseconds
+
+ protected static enum DeviceStatus {
+ INITIALIZED,
+ POLLING,
+ STOPPED
+ }
+
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ protected T address;
+ private @Nullable S handler;
+ private @Nullable InsteonModem modem;
+ private @Nullable ProductData productData;
+ private DeviceStatus status = DeviceStatus.INITIALIZED;
+ private Map features = new LinkedHashMap<>();
+ private Map flags = new HashMap<>();
+ private Queue requestQueue = new PriorityQueue<>();
+ private Map requestQueueHash = new HashMap<>();
+ private @Nullable DeviceFeature featureQueried;
+ private long pollInterval = -1L; // in milliseconds
+ private volatile long lastRequestQueued = 0L;
+ private volatile long lastRequestSent = 0L;
+
+ public BaseDevice(T address) {
+ this.address = address;
+ }
+
+ @Override
+ public T getAddress() {
+ return address;
+ }
+
+ public @Nullable S getHandler() {
+ return handler;
+ }
+
+ public @Nullable InsteonModem getModem() {
+ return modem;
+ }
+
+ @Override
+ public @Nullable ProductData getProductData() {
+ return productData;
+ }
+
+ @Override
+ public @Nullable DeviceType getType() {
+ return Optional.ofNullable(productData).map(ProductData::getDeviceType).orElse(null);
+ }
+
+ protected DeviceStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public List getFeatures() {
+ synchronized (features) {
+ return features.values().stream().toList();
+ }
+ }
+
+ @Override
+ public @Nullable DeviceFeature getFeature(String name) {
+ synchronized (features) {
+ return features.get(name);
+ }
+ }
+
+ public boolean hasFeatures() {
+ return !getFeatures().isEmpty();
+ }
+
+ public boolean hasFeature(String name) {
+ return getFeature(name) != null;
+ }
+
+ public double getLastMsgValueAsDouble(String name, double defaultValue) {
+ return Optional.ofNullable(getFeature(name)).map(DeviceFeature::getLastMsgValue).map(Double::doubleValue)
+ .orElse(defaultValue);
+ }
+
+ public int getLastMsgValueAsInteger(String name, int defaultValue) {
+ return Optional.ofNullable(getFeature(name)).map(DeviceFeature::getLastMsgValue).map(Double::intValue)
+ .orElse(defaultValue);
+ }
+
+ public @Nullable State getFeatureState(String name) {
+ return Optional.ofNullable(getFeature(name)).map(DeviceFeature::getState).orElse(null);
+ }
+
+ public boolean getFlag(String key, boolean def) {
+ synchronized (flags) {
+ return flags.getOrDefault(key, def);
+ }
+ }
+
+ public @Nullable DeviceFeature getFeatureQueried() {
+ synchronized (requestQueue) {
+ return featureQueried;
+ }
+ }
+
+ public void setModem(@Nullable InsteonModem modem) {
+ this.modem = modem;
+ }
+
+ public void setAddress(T address) {
+ this.address = address;
+ }
+
+ public void setHandler(S handler) {
+ this.handler = handler;
+ }
+
+ public void setProductData(ProductData productData) {
+ logger.trace("setting product data for {} to {}", address, productData);
+ this.productData = productData;
+ }
+
+ protected void setStatus(DeviceStatus status) {
+ this.status = status;
+ }
+
+ public void setFlag(String key, boolean value) {
+ logger.trace("setting {} flag for {} to {}", key, address, value);
+ synchronized (flags) {
+ flags.put(key, value);
+ }
+ }
+
+ public void setFlags(Map flags) {
+ flags.forEach(this::setFlag);
+ }
+
+ public void setFeatureQueried(@Nullable DeviceFeature featureQueried) {
+ synchronized (requestQueue) {
+ this.featureQueried = featureQueried;
+ }
+ }
+
+ public void setPollInterval(long pollInterval) {
+ if (pollInterval > 0) {
+ logger.trace("setting poll interval for {} to {}", address, pollInterval);
+ this.pollInterval = pollInterval;
+ }
+ }
+
+ @Override
+ public String toString() {
+ String s = address.toString();
+ if (productData != null) {
+ s += "|" + productData;
+ } else {
+ s += "|unknown device";
+ }
+ for (DeviceFeature feature : getFeatures()) {
+ s += "|" + feature;
+ }
+ return s;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ InsteonDevice other = (InsteonDevice) obj;
+ return address.equals(other.address);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + address.hashCode();
+ return result;
+ }
+
+ /**
+ * Returns if device is pollable
+ *
+ * @return true if has a pollable feature
+ */
+ public boolean isPollable() {
+ return getFeatures().stream().anyMatch(DeviceFeature::isPollable);
+ }
+
+ /**
+ * Starts polling this device
+ */
+ public void startPolling() {
+ InsteonModem modem = getModem();
+ // start polling if currently disabled
+ if (modem != null && getStatus() != DeviceStatus.POLLING) {
+ getFeatures().forEach(DeviceFeature::initializeQueryStatus);
+ int ndbes = modem.getDB().getEntries().size();
+ modem.getPollManager().startPolling(this, pollInterval, ndbes);
+ setStatus(DeviceStatus.POLLING);
+ }
+ }
+
+ /**
+ * Stops polling this device
+ */
+ public void stopPolling() {
+ InsteonModem modem = getModem();
+ // stop polling if currently enabled
+ if (modem != null && getStatus() == DeviceStatus.POLLING) {
+ modem.getPollManager().stopPolling(this);
+ clearRequestQueue();
+ setStatus(DeviceStatus.STOPPED);
+ }
+ }
+
+ /**
+ * Polls this device
+ *
+ * @param delay scheduling delay (in milliseconds)
+ */
+ @Override
+ public void doPoll(long delay) {
+ schedulePoll(delay, feature -> true);
+ }
+
+ /**
+ * Polls a specific feature for this device
+ *
+ * @param name name of the feature to poll
+ * @param delay scheduling delay (in milliseconds)
+ * @return poll message
+ */
+ public @Nullable Msg pollFeature(String name, long delay) {
+ return Optional.ofNullable(getFeature(name)).map(feature -> feature.doPoll(delay)).orElse(null);
+ }
+
+ /**
+ * Schedules polling for this device
+ *
+ * @param delay scheduling delay (in milliseconds)
+ * @param featureFilter feature filter to apply
+ * @return delay spacing
+ */
+ protected long schedulePoll(long delay, Predicate featureFilter) {
+ long spacing = 0;
+ for (DeviceFeature feature : getFeatures()) {
+ // skip if is event feature or feature filter doesn't match
+ if (feature.isEventFeature() || !featureFilter.test(feature)) {
+ continue;
+ }
+ // poll feature with listeners or never queried before
+ if (feature.hasListeners() || feature.getQueryStatus() == QueryStatus.NEVER_QUERIED) {
+ Msg msg = feature.doPoll(delay + spacing);
+ if (msg != null) {
+ spacing += msg.getQuietTime();
+ }
+ }
+ }
+ return spacing;
+ }
+
+ /**
+ * Clears request queue
+ */
+ protected void clearRequestQueue() {
+ logger.trace("clearing request queue for {}", address);
+
+ synchronized (requestQueue) {
+ requestQueue.clear();
+ requestQueueHash.clear();
+ }
+ }
+
+ /**
+ * Instantiates features for this device based on a device type
+ *
+ * @param deviceType device type to instantiate features from
+ */
+ protected void instantiateFeatures(DeviceType deviceType) {
+ for (FeatureEntry featureEntry : deviceType.getFeatures()) {
+ DeviceFeature feature = DeviceFeature.makeDeviceFeature(this, featureEntry.getName(),
+ featureEntry.getType(), featureEntry.getParameters());
+ if (feature == null) {
+ logger.warn("device type {} references unknown feature type {}", deviceType.getName(),
+ featureEntry.getType());
+ } else {
+ addFeature(feature);
+ }
+ }
+ for (FeatureEntry featureEntry : deviceType.getFeatureGroups()) {
+ DeviceFeature feature = getFeature(featureEntry.getName());
+ if (feature == null) {
+ logger.warn("device type {} references unknown feature group {}", deviceType.getName(),
+ featureEntry.getName());
+ } else {
+ connectFeatures(feature, featureEntry.getConnectedFeatures());
+ }
+ }
+ }
+
+ /**
+ * Adds feature to this device
+ *
+ * @param feature device feature to add
+ */
+ private void addFeature(DeviceFeature feature) {
+ synchronized (features) {
+ features.put(feature.getName(), feature);
+ }
+ }
+
+ /**
+ * Connects group features to its parent
+ *
+ * @param groupFeature group feature to connect to
+ * @param features connected features part of that group feature
+ */
+ private void connectFeatures(DeviceFeature groupFeature, List features) {
+ for (String name : features) {
+ DeviceFeature feature = getFeature(name);
+ if (feature == null) {
+ logger.warn("group feature {} references unknown feature {}", groupFeature.getName(), name);
+ } else {
+ logger.trace("{} connected feature: {}", groupFeature.getName(), feature.getName());
+ feature.addParameters(groupFeature.getParameters());
+ feature.setGroupFeature(groupFeature);
+ feature.setPollHandler(null);
+ groupFeature.addConnectedFeature(feature);
+ }
+ }
+ }
+
+ /**
+ * Resets features query status for this device
+ */
+ public void resetFeaturesQueryStatus() {
+ if (getStatus() == DeviceStatus.POLLING) {
+ logger.trace("resetting device features query status for {}", address);
+
+ DeviceFeature featureQueried = getFeatureQueried();
+ getFeatures().stream().filter(feature -> !feature.equals(featureQueried))
+ .forEach(DeviceFeature::initializeQueryStatus);
+ }
+ }
+
+ /**
+ * Handles incoming message for this device by forwarding
+ * it to all features that this device supports
+ *
+ * @param msg the incoming message
+ */
+ @Override
+ public void handleMessage(Msg msg) {
+ getFeatures().stream().filter(feature -> feature.handleMessage(msg)).findFirst().ifPresent(feature -> {
+ logger.trace("handled reply of direct for {}", feature.getName());
+ // mark feature queried as processed and answered
+ setFeatureQueried(null);
+ feature.setQueryMessage(null);
+ feature.setQueryStatus(QueryStatus.QUERY_ANSWERED);
+ });
+ }
+
+ /**
+ * Sends a message after a delay to this device
+ *
+ * @param msg the message to be sent
+ * @param feature device feature associated to the message
+ * @param delay time (in milliseconds) to delay before sending message
+ */
+ @Override
+ public void sendMessage(Msg msg, DeviceFeature feature, long delay) {
+ addDeviceRequest(msg, feature, delay);
+ }
+
+ /**
+ * Adds a request for this device
+ *
+ * @param msg message to be sent
+ * @param feature device feature that sent this message
+ * @param delay time (in milliseconds) to delay before sending message
+ */
+ protected void addDeviceRequest(Msg msg, DeviceFeature feature, long delay) {
+ logger.trace("enqueuing request with delay {} msec", delay);
+
+ synchronized (requestQueue) {
+ DeviceRequest request = new DeviceRequest(feature, msg, delay);
+ DeviceRequest prevRequest = requestQueueHash.get(msg);
+ if (prevRequest != null) {
+ logger.trace("overwriting existing request for {}: {}", feature.getName(), msg);
+ requestQueue.remove(prevRequest);
+ requestQueueHash.remove(msg);
+ }
+ requestQueue.add(request);
+ requestQueueHash.put(msg, request);
+ }
+ InsteonModem modem = getModem();
+ if (modem != null) {
+ modem.getRequestManager().addQueue(this, delay);
+ }
+ }
+
+ /**
+ * Handles next request for this device
+ *
+ * @return wait time (in milliseconds) before processing the subsequent request
+ */
+ @Override
+ public long handleNextRequest() {
+ long now = System.currentTimeMillis();
+ // wait for feature queried to complete
+ long waitTime = checkFeatureQueried(now);
+ if (waitTime > 0) {
+ return waitTime;
+ }
+
+ synchronized (requestQueue) {
+ // take the next request off the queue
+ DeviceRequest request = requestQueue.poll();
+ if (request == null) {
+ return 0L;
+ }
+ // get requested feature and message
+ DeviceFeature feature = request.getFeature();
+ Msg msg = request.getMessage();
+ // remove request from queue hash
+ requestQueueHash.remove(msg);
+ // set last request queued time
+ lastRequestQueued = now;
+ // set feature queried for non-broadcast request message
+ if (!msg.isAllLinkBroadcast()) {
+ logger.trace("request taken off direct for {}: {}", feature.getName(), msg);
+ // mark requested feature query status as queued
+ feature.setQueryStatus(QueryStatus.QUERY_QUEUED);
+ // store requested feature query message
+ feature.setQueryMessage(msg);
+ // set feature queried
+ setFeatureQueried(feature);
+ } else {
+ logger.trace("request taken off bcast for {}: {}", feature.getName(), msg);
+ }
+ // write message
+ InsteonModem modem = getModem();
+ if (modem != null) {
+ try {
+ modem.writeMessage(msg);
+ } catch (IOException e) {
+ logger.warn("message write failed for msg: {}", msg, e);
+ }
+ }
+ // determine the wait time for the next request
+ long quietTime = msg.getQuietTime();
+ long nextExpTime = Optional.ofNullable(requestQueue.peek()).map(DeviceRequest::getExpirationTime)
+ .orElse(0L);
+ long nextTime = Math.max(now + quietTime, nextExpTime);
+ logger.trace("next request queue processed in {} msec, quiettime {} msec", nextTime - now, quietTime);
+ return nextTime;
+ }
+ }
+
+ /**
+ * Checks feature queried status
+ *
+ * @param now the current time
+ * @return wait time if necessary otherwise 0
+ */
+ private long checkFeatureQueried(long now) {
+ DeviceFeature feature = getFeatureQueried();
+ if (feature != null) {
+ QueryStatus queryStatus = feature.getQueryStatus();
+ switch (queryStatus) {
+ case QUERY_QUEUED:
+ // wait for feature queried request to be sent
+ long maxQueueTime = lastRequestQueued + REQUEST_QUEUE_TIMEOUT;
+ if (maxQueueTime > now) {
+ logger.trace("still waiting for {} query to be sent to {} for another {} msec",
+ feature.getName(), address, maxQueueTime - now);
+ return now + 1000L; // retry in 1000 ms
+ }
+ logger.debug("gave up waiting for {} query to be sent to {}", feature.getName(), address);
+ // reset feature queried as never queried
+ feature.setQueryMessage(null);
+ feature.setQueryStatus(QueryStatus.NEVER_QUERIED);
+ break;
+ case QUERY_SENT:
+ case QUERY_ACKED:
+ // wait for the feature queried to be answered
+ long maxAckTime = lastRequestSent + DIRECT_ACK_TIMEOUT;
+ if (maxAckTime > now) {
+ logger.trace("still waiting for {} query reply from {} for another {} msec", feature.getName(),
+ address, maxAckTime - now);
+ return now + 500L; // retry in 500 ms
+ }
+ logger.debug("gave up waiting for {} query reply from {}", feature.getName(), address);
+ // reset feature queried as never queried
+ feature.setQueryMessage(null);
+ feature.setQueryStatus(QueryStatus.NEVER_QUERIED);
+ break;
+ default:
+ logger.debug("unexpected feature {} query status {} for {}", feature.getName(), queryStatus,
+ address);
+ }
+ // reset feature queried otheriwse
+ setFeatureQueried(null);
+ }
+ return 0L;
+ }
+
+ /**
+ * Notifies that a message request was replied for this device
+ *
+ * @param msg the message received
+ */
+ @Override
+ public void requestReplied(Msg msg) {
+ DeviceFeature feature = getFeatureQueried();
+ if (feature != null && feature.isMyReply(msg)) {
+ if (msg.isReplyAck()) {
+ // mark feature queried as acked
+ feature.setQueryStatus(QueryStatus.QUERY_ACKED);
+ } else {
+ logger.debug("got a reply nack msg: {}", msg);
+ // mark feature queried as processed and answered
+ setFeatureQueried(null);
+ feature.setQueryMessage(null);
+ feature.setQueryStatus(QueryStatus.QUERY_ANSWERED);
+ }
+ }
+ }
+
+ /**
+ * Notifies that a message request was sent to this device
+ *
+ * @param msg the message sent
+ * @param time the time the request was sent
+ */
+ @Override
+ public void requestSent(Msg msg, long time) {
+ DeviceFeature feature = getFeatureQueried();
+ if (feature != null && msg.equals(feature.getQueryMessage())) {
+ // mark feature queried as pending
+ feature.setQueryStatus(QueryStatus.QUERY_SENT);
+ // set last request sent time
+ lastRequestSent = time;
+ }
+ }
+
+ /**
+ * Refreshes this device
+ */
+ @Override
+ public void refresh() {
+ logger.trace("refreshing device {}", address);
+ @Nullable
+ S handler = getHandler();
+ if (handler != null) {
+ handler.refresh();
+ }
+ }
+
+ /**
+ * Class that represents a device request
+ */
+ protected static class DeviceRequest implements Comparable {
+ private DeviceFeature feature;
+ private Msg msg;
+ private long expirationTime;
+
+ public DeviceRequest(DeviceFeature feature, Msg msg, long delay) {
+ this.feature = feature;
+ this.msg = msg;
+ setExpirationTime(delay);
+ }
+
+ public DeviceFeature getFeature() {
+ return feature;
+ }
+
+ public Msg getMessage() {
+ return msg;
+ }
+
+ public long getExpirationTime() {
+ return expirationTime;
+ }
+
+ public void setExpirationTime(long delay) {
+ this.expirationTime = System.currentTimeMillis() + delay;
+ }
+
+ @Override
+ public int compareTo(DeviceRequest other) {
+ return (int) (expirationTime - other.expirationTime);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DefaultLink.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DefaultLink.java
new file mode 100644
index 0000000000000..a10413acaf826
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DefaultLink.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.insteon.internal.device.database.LinkDBRecord;
+import org.openhab.binding.insteon.internal.device.database.ModemDBRecord;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+
+/**
+ * The {@link DefaultLink} represents a device default link
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class DefaultLink {
+ private String name;
+ private LinkDBRecord linkDBRecord;
+ private ModemDBRecord modemDBRecord;
+ private List commands;
+
+ public DefaultLink(String name, LinkDBRecord linkDBRecord, ModemDBRecord modemDBRecord, List commands) {
+ this.name = name;
+ this.linkDBRecord = linkDBRecord;
+ this.modemDBRecord = modemDBRecord;
+ this.commands = commands;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public LinkDBRecord getLinkDBRecord() {
+ return linkDBRecord;
+ }
+
+ public ModemDBRecord getModemDBRecord() {
+ return modemDBRecord;
+ }
+
+ public List getCommands() {
+ return commands;
+ }
+
+ @Override
+ public String toString() {
+ String s = name + "|linkDB:" + linkDBRecord + "|modemDB:" + modemDBRecord;
+ if (!commands.isEmpty()) {
+ s += "|commands:" + commands;
+ }
+ return s;
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/Device.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/Device.java
new file mode 100644
index 0000000000000..1a4be063c832c
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/Device.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+
+/**
+ * Interface for classes that represent a device
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public interface Device {
+ /**
+ * Returns the address for this device
+ *
+ * @return the device address
+ */
+ public DeviceAddress getAddress();
+
+ /**
+ * Returns the product data for this device
+ *
+ * @return the device product data if defined, otherwise null
+ */
+ public @Nullable ProductData getProductData();
+
+ /**
+ * Returns the type for this device
+ *
+ * @return the device type if defined, otherwise null
+ */
+ public @Nullable DeviceType getType();
+
+ /**
+ * Returns a feature based on name for this device
+ *
+ * @param name the device feature name to match
+ * @return the device feature if found, otherwise null
+ */
+ public @Nullable DeviceFeature getFeature(String name);
+
+ /**
+ * Returns the list of features for this device
+ *
+ * @return the list of device features
+ */
+ public List getFeatures();
+
+ /**
+ * Polls this device
+ *
+ * @param delay scheduling delay (in milliseconds)
+ */
+ public void doPoll(long delay);
+
+ /**
+ * Handles an incoming message for this device
+ *
+ * @param msg the incoming message
+ */
+ public void handleMessage(Msg msg);
+
+ /**
+ * Sends a message after a delay to this device
+ *
+ * @param msg the message to be sent
+ * @param feature device feature associated to the message
+ * @param delay time (in milliseconds) to delay before sending message
+ */
+ public void sendMessage(Msg msg, DeviceFeature feature, long delay);
+
+ /**
+ * Handles next request for this device
+ *
+ * @return time (in milliseconds) before processing the subsequent request
+ */
+ public long handleNextRequest();
+
+ /**
+ * Notifies that a message request was replied for this device
+ *
+ * @param msg the message received
+ */
+ public void requestReplied(Msg msg);
+
+ /**
+ * Notifies that a message request was sent to this device
+ *
+ * @param msg the message sent
+ * @param time the time the request was sent
+ */
+ public void requestSent(Msg msg, long time);
+
+ /**
+ * Refreshes this device
+ */
+ public void refresh();
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceAddress.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceAddress.java
new file mode 100644
index 0000000000000..2294b6e7fedd5
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceAddress.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Interface for classes that represent a device address
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public interface DeviceAddress {
+ @Override
+ public String toString();
+
+ @Override
+ public boolean equals(@Nullable Object obj);
+
+ @Override
+ public int hashCode();
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceCache.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceCache.java
new file mode 100644
index 0000000000000..136b40178621e
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceCache.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.device.database.DatabaseCache;
+import org.openhab.binding.insteon.internal.device.database.LinkDB;
+import org.openhab.binding.insteon.internal.device.database.ModemDB;
+import org.openhab.binding.insteon.internal.device.feature.FeatureCache;
+
+/**
+ * The {@link DeviceCache} represents a device cache
+ *
+ * @author Jeremy Setton - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceCache {
+ private @Nullable ProductData productData;
+ private @Nullable InsteonEngine engine;
+ private @Nullable DatabaseCache database;
+ private @Nullable Map features;
+
+ public @Nullable ProductData getProductData() {
+ return productData;
+ }
+
+ public InsteonEngine getInsteonEngine() {
+ return Objects.requireNonNullElse(engine, InsteonEngine.UNKNOWN);
+ }
+
+ public @Nullable DatabaseCache getDatabaseCache() {
+ return database;
+ }
+
+ public Map getFeatureCaches() {
+ return Objects.requireNonNullElse(features, Collections.emptyMap());
+ }
+
+ /**
+ * Loads this device cache into a device
+ *
+ * @param device the device to use
+ */
+ public void load(Device device) {
+ // load device feature caches
+ getFeatureCaches().forEach((name, cache) -> {
+ DeviceFeature feature = device.getFeature(name);
+ if (feature != null) {
+ cache.load(feature);
+ }
+ });
+
+ if (device instanceof InsteonDevice insteonDevice) {
+ // set device insteon engine if known
+ InsteonEngine engine = getInsteonEngine();
+ if (engine != InsteonEngine.UNKNOWN) {
+ insteonDevice.setInsteonEngine(engine);
+ }
+
+ // load device database cache if defined
+ DatabaseCache database = getDatabaseCache();
+ if (database != null) {
+ database.load(insteonDevice.getLinkDB());
+ }
+ } else if (device instanceof InsteonModem insteonModem) {
+ // load modem database cache if defined
+ DatabaseCache database = getDatabaseCache();
+ if (database != null) {
+ database.load(insteonModem.getDB());
+ }
+ }
+ }
+
+ /**
+ * Class that represents a device cache builder
+ */
+ public static class Builder {
+ private final DeviceCache cache = new DeviceCache();
+
+ private Builder() {
+ }
+
+ public Builder withProductData(@Nullable ProductData productData) {
+ cache.productData = productData;
+ return this;
+ }
+
+ public Builder withInsteonEngine(InsteonEngine engine) {
+ cache.engine = engine;
+ return this;
+ }
+
+ public Builder withDatabase(LinkDB linkDB) {
+ cache.database = DatabaseCache.builder().withDatabaseDelta(linkDB.getDatabaseDelta())
+ .withReload(linkDB.shouldReload()).withRecords(linkDB.getRecords()).build();
+ return this;
+ }
+
+ public Builder withDatabase(ModemDB modemDB) {
+ cache.database = DatabaseCache.builder().withProducts(modemDB.getProducts())
+ .withRecords(modemDB.getRecords()).build();
+ return this;
+ }
+
+ public Builder withFeatures(List features) {
+ cache.features = features.stream().filter(feature -> !feature.isEventFeature() && !feature.isGroupFeature())
+ .collect(Collectors.toMap(DeviceFeature::getName, feature -> FeatureCache.builder()
+ .withState(feature.getState()).withLastMsgValue(feature.getLastMsgValue()).build()));
+ return this;
+ }
+
+ public DeviceCache build() {
+ return cache;
+ }
+ }
+
+ /**
+ * Factory method for creating a device cache builder
+ *
+ * @return the newly created device cache builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java
index c31ce17a11852..7d36b01e22924 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceFeature.java
@@ -12,25 +12,37 @@
*/
package org.openhab.binding.insteon.internal.device;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
+import static org.openhab.binding.insteon.internal.InsteonBindingConstants.*;
+
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
-import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
-import org.openhab.binding.insteon.internal.message.Msg;
-import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
+import org.openhab.binding.insteon.internal.device.feature.CommandHandler;
+import org.openhab.binding.insteon.internal.device.feature.FeatureListener;
+import org.openhab.binding.insteon.internal.device.feature.FeatureTemplate;
+import org.openhab.binding.insteon.internal.device.feature.FeatureTemplateRegistry;
+import org.openhab.binding.insteon.internal.device.feature.MessageDispatcher;
+import org.openhab.binding.insteon.internal.device.feature.MessageHandler;
+import org.openhab.binding.insteon.internal.device.feature.PollHandler;
+import org.openhab.binding.insteon.internal.transport.message.FieldException;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+import org.openhab.binding.insteon.internal.utils.ParameterParser;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
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;
@@ -48,8 +60,8 @@
* 3) CommandHandler: translates commands from the openhab bus into an Insteon message.
* 4) PollHandler: creates an Insteon message to query the DeviceFeature
*
- * Lastly, DeviceFeatureListeners can register with the DeviceFeature to get notifications when
- * the state of a feature has changed. In practice, a DeviceFeatureListener corresponds to an
+ * Lastly, InsteonChannelHandler can register with the DeviceFeature to get notifications when
+ * the state of a feature is updated. In practice, a InsteonChannelHandler corresponds to an
* openHAB item.
*
* The character of a DeviceFeature is thus given by a set of message and command handlers.
@@ -61,386 +73,667 @@
* @author Daniel Pfrommer - Initial contribution
* @author Bernd Pfrommer - openHAB 1 insteonplm binding
* @author Rob Nielsen - Port to openHAB 2 insteon binding
+ * @author Jeremy Setton - Rewrite insteon binding
*/
@NonNullByDefault
public class DeviceFeature {
- public enum QueryStatus {
+ public static enum QueryStatus {
NEVER_QUERIED,
- QUERY_PENDING,
- QUERY_ANSWERED
- }
-
- private static final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
-
- private static Map features = new HashMap<>();
-
- private InsteonDevice device = new InsteonDevice();
- private String name = "INVALID_FEATURE_NAME";
- private boolean isStatus = false;
- private int directAckTimeout = 6000;
- private QueryStatus queryStatus = QueryStatus.NEVER_QUERIED;
-
- private MessageHandler defaultMsgHandler = new MessageHandler.DefaultMsgHandler(this);
- private CommandHandler defaultCommandHandler = new CommandHandler.WarnCommandHandler(this);
- private @Nullable PollHandler pollHandler = null;
- private @Nullable MessageDispatcher dispatcher = null;
-
- private Map msgHandlers = new HashMap<>();
- private Map, @Nullable CommandHandler> commandHandlers = new HashMap<>();
- private List listeners = new ArrayList<>();
+ QUERY_SCHEDULED,
+ QUERY_QUEUED,
+ QUERY_SENT,
+ QUERY_ACKED,
+ QUERY_ANSWERED,
+ NOT_POLLABLE
+ }
+
+ private final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
+
+ private String name;
+ private String type;
+ private Device device;
+ private QueryStatus queryStatus = QueryStatus.NOT_POLLABLE;
+ private State state = UnDefType.NULL;
+ private @Nullable Double lastMsgValue;
+ private @Nullable Msg queryMsg;
+
+ private MessageHandler defaultMsgHandler = MessageHandler.makeDefaultHandler(this);
+ private CommandHandler defaultCommandHandler = CommandHandler.makeDefaultHandler(this);
+ private @Nullable PollHandler pollHandler;
+ private @Nullable MessageDispatcher dispatcher;
+ private @Nullable DeviceFeature groupFeature;
+
+ private Map parameters = new HashMap<>();
+ private Map msgHandlers = new HashMap<>();
+ private Map commandHandlers = new HashMap<>();
private List connectedFeatures = new ArrayList<>();
+ private Set listeners = new CopyOnWriteArraySet<>();
/**
* Constructor
*
- * @param device Insteon device to which this feature belongs
- * @param name descriptive name for that feature
- */
- public DeviceFeature(InsteonDevice device, String name) {
- this.name = name;
- setDevice(device);
- }
-
- /**
- * Constructor
- *
- * @param name descriptive name of the feature
+ * @param name feature name
+ * @param type feature type
+ * @param device feature device
*/
- public DeviceFeature(String name) {
+ public DeviceFeature(String name, String type, Device device) {
this.name = name;
+ this.type = type;
+ this.device = device;
}
- // various simple getters
public String getName() {
return name;
}
+ public String getType() {
+ return type;
+ }
+
+ public Device getDevice() {
+ return device;
+ }
+
+ public Map getParameters() {
+ synchronized (parameters) {
+ return parameters;
+ }
+ }
+
+ public @Nullable String getParameter(String key) {
+ synchronized (parameters) {
+ return parameters.get(key);
+ }
+ }
+
+ public boolean hasParameter(String key) {
+ synchronized (parameters) {
+ return parameters.containsKey(key);
+ }
+ }
+
+ public boolean getParameterAsBoolean(String key, boolean defaultValue) {
+ return ParameterParser.getParameterAsOrDefault(getParameter(key), Boolean.class, defaultValue);
+ }
+
+ public int getParameterAsInteger(String key, int defaultValue) {
+ return ParameterParser.getParameterAsOrDefault(getParameter(key), Integer.class, defaultValue);
+ }
+
+ public synchronized @Nullable Double getLastMsgValue() {
+ return lastMsgValue;
+ }
+
+ public double getLastMsgValueAsDouble(double defaultValue) {
+ return Optional.ofNullable(getLastMsgValue()).map(Double::doubleValue).orElse(defaultValue);
+ }
+
+ public int getLastMsgValueAsInteger(int defaultValue) {
+ return Optional.ofNullable(getLastMsgValue()).map(Double::intValue).orElse(defaultValue);
+ }
+
+ public synchronized @Nullable Msg getQueryMessage() {
+ return queryMsg;
+ }
+
+ public int getQueryCommand() {
+ Msg queryMsg = getQueryMessage();
+ if (queryMsg != null) {
+ try {
+ return queryMsg.getInt("command1");
+ } catch (FieldException e) {
+ logger.warn("{}:{} error parsing msg {}", device.getAddress(), name, queryMsg, e);
+ }
+ }
+ return -1;
+ }
+
public synchronized QueryStatus getQueryStatus() {
return queryStatus;
}
- public InsteonDevice getDevice() {
- return device;
+ public synchronized State getState() {
+ return state;
}
- public boolean isFeatureGroup() {
+ public boolean isGroupFeature() {
return !connectedFeatures.isEmpty();
}
+ public boolean isPartOfGroupFeature() {
+ return groupFeature != null;
+ }
+
+ public boolean isControllerFeature() {
+ String linkType = getParameter("link");
+ return "both".equals(linkType) || "controller".equals(linkType);
+ }
+
+ public boolean isResponderFeature() {
+ String linkType = getParameter("link");
+ return "both".equals(linkType) || "responder".equals(linkType);
+ }
+
+ public boolean isControllerOrResponderFeature() {
+ return isControllerFeature() || isResponderFeature();
+ }
+
+ public boolean isEventFeature() {
+ return getParameterAsBoolean("event", false);
+ }
+
+ public boolean isHiddenFeature() {
+ return getParameterAsBoolean("hidden", false);
+ }
+
public boolean isStatusFeature() {
- return isStatus;
+ return getParameterAsBoolean("status", false);
+ }
+
+ public int getGroup() {
+ return getParameterAsInteger("group", 1);
}
- public int getDirectAckTimeout() {
- return directAckTimeout;
+ public int getComponentId() {
+ int componentId = 0;
+ if (device instanceof InsteonDevice insteonDevice) {
+ // use feature group as component id if device has more than one controller or responder feature,
+ // othewise use the component id of the link db first record
+ if (insteonDevice.getControllerOrResponderFeatures().size() > 1) {
+ componentId = getGroup();
+ } else {
+ componentId = insteonDevice.getLinkDB().getFirstRecordComponentId();
+ }
+ }
+ return componentId;
}
public MessageHandler getDefaultMsgHandler() {
return defaultMsgHandler;
}
- public Map getMsgHandlers() {
- return this.msgHandlers;
+ public @Nullable MessageHandler getMsgHandler(int command, int group) {
+ synchronized (msgHandlers) {
+ return msgHandlers.get(MessageHandler.generateId(command, group));
+ }
+ }
+
+ public MessageHandler getOrDefaultMsgHandler(int command, int group) {
+ synchronized (msgHandlers) {
+ return msgHandlers.getOrDefault(MessageHandler.generateId(command, group), defaultMsgHandler);
+ }
+ }
+
+ public MessageHandler getOrDefaultMsgHandler(int command) {
+ return getOrDefaultMsgHandler(command, -1);
+ }
+
+ public CommandHandler getOrDefaultCommandHandler(String key) {
+ synchronized (commandHandlers) {
+ return commandHandlers.getOrDefault(key, defaultCommandHandler);
+ }
+ }
+
+ public @Nullable MessageDispatcher getMsgDispatcher() {
+ return dispatcher;
+ }
+
+ public @Nullable PollHandler getPollHandler() {
+ return pollHandler;
+ }
+
+ public boolean isPollable() {
+ PollHandler pollHandler = getPollHandler();
+ return pollHandler != null && pollHandler.makeMsg() != null;
+ }
+
+ public @Nullable DeviceFeature getGroupFeature() {
+ return groupFeature;
}
public List getConnectedFeatures() {
- return (connectedFeatures);
+ synchronized (connectedFeatures) {
+ return connectedFeatures;
+ }
}
- // various simple setters
- public void setStatusFeature(boolean f) {
- isStatus = f;
+ public boolean hasControllerFeatures() {
+ return isControllerFeature() || getConnectedFeatures().stream().anyMatch(DeviceFeature::hasControllerFeatures);
}
- public void setPollHandler(@Nullable PollHandler h) {
- pollHandler = h;
+ public boolean hasResponderFeatures() {
+ return isResponderFeature() || getConnectedFeatures().stream().anyMatch(DeviceFeature::hasResponderFeatures);
}
- public void setDevice(InsteonDevice d) {
- device = d;
+ public boolean hasListeners() {
+ return !listeners.isEmpty() || getConnectedFeatures().stream().anyMatch(DeviceFeature::hasListeners);
}
- public void setMessageDispatcher(@Nullable MessageDispatcher md) {
- dispatcher = md;
+ public void setMessageDispatcher(@Nullable MessageDispatcher dispatcher) {
+ this.dispatcher = dispatcher;
}
- public void setDefaultCommandHandler(CommandHandler ch) {
- defaultCommandHandler = ch;
+ public void setPollHandler(@Nullable PollHandler pollHandler) {
+ this.pollHandler = pollHandler;
}
- public void setDefaultMsgHandler(MessageHandler mh) {
- defaultMsgHandler = mh;
+ public void setDefaultCommandHandler(CommandHandler defaultCommandHandler) {
+ this.defaultCommandHandler = defaultCommandHandler;
}
- public synchronized void setQueryStatus(QueryStatus status) {
- logger.trace("{} set query status to: {}", name, status);
- queryStatus = status;
+ public void setDefaultMsgHandler(MessageHandler defaultMsgHandler) {
+ this.defaultMsgHandler = defaultMsgHandler;
}
- public void setTimeout(@Nullable String s) {
- if (s != null && !s.isEmpty()) {
- try {
- directAckTimeout = Integer.parseInt(s);
- logger.trace("ack timeout set to {}", directAckTimeout);
- } catch (NumberFormatException e) {
- logger.warn("invalid number for timeout: {}", s);
+ public void setGroupFeature(DeviceFeature groupFeature) {
+ this.groupFeature = groupFeature;
+ }
+
+ public synchronized void setLastMsgValue(double lastMsgValue) {
+ logger.trace("{}:{} setting last message value to: {}", device.getAddress(), name, lastMsgValue);
+ this.lastMsgValue = lastMsgValue;
+ }
+
+ public synchronized void setQueryMessage(@Nullable Msg queryMsg) {
+ this.queryMsg = queryMsg;
+ }
+
+ public synchronized void setQueryStatus(QueryStatus queryStatus) {
+ logger.trace("{}:{} setting query status to: {}", device.getAddress(), name, queryStatus);
+ this.queryStatus = queryStatus;
+ }
+
+ public synchronized void setState(State state) {
+ logger.trace("{}:{} setting state to: {}", device.getAddress(), name, state);
+ this.state = state;
+ }
+
+ public void initializeQueryStatus() {
+ // set query status to never queried if feature pollable,
+ // otherwise to not pollable if not already in that state
+ if (isPollable()) {
+ setQueryStatus(QueryStatus.NEVER_QUERIED);
+ } else if (queryStatus != QueryStatus.NOT_POLLABLE) {
+ setQueryStatus(QueryStatus.NOT_POLLABLE);
+ }
+ }
+
+ public void addParameters(Map params) {
+ synchronized (parameters) {
+ parameters.putAll(params);
+ }
+ // reset message handler map ids if new group parameter added
+ if (params.containsKey(PARAMETER_GROUP)) {
+ resetMessageHandlerIds();
+ }
+ }
+
+ public void addMessageHandler(String key, MessageHandler handler) {
+ synchronized (msgHandlers) {
+ if (msgHandlers.putIfAbsent(key, handler) != null) {
+ logger.warn("{}: ignoring duplicate message handler: {}->{}", type, key, handler);
}
}
}
- /**
- * Add a listener (item) to a device feature
- *
- * @param l the listener
- */
- public void addListener(DeviceFeatureListener l) {
- synchronized (listeners) {
- for (DeviceFeatureListener m : listeners) {
- if (m.getItemName().equals(l.getItemName())) {
- return;
- }
+ public void addCommandHandler(String key, CommandHandler handler) {
+ synchronized (commandHandlers) {
+ if (commandHandlers.putIfAbsent(key, handler) != null) {
+ logger.warn("{}: ignoring duplicate command handler: {}->{}", type, key, handler);
+ }
+ }
+ }
+
+ private void resetMessageHandlerIds() {
+ synchronized (msgHandlers) {
+ if (!msgHandlers.isEmpty()) {
+ Map handlers = msgHandlers.values().stream()
+ .collect(Collectors.toMap(MessageHandler::getId, Function.identity()));
+ msgHandlers.clear();
+ msgHandlers.putAll(handlers);
}
- listeners.add(l);
}
}
+ public void addConnectedFeature(DeviceFeature feature) {
+ synchronized (connectedFeatures) {
+ connectedFeatures.add(feature);
+ }
+ }
+
+ public void registerListener(FeatureListener listener) {
+ listeners.add(listener);
+ }
+
+ public void unregisterListener(FeatureListener listener) {
+ listeners.remove(listener);
+ }
+
/**
- * Adds a connected feature such that this DeviceFeature can
- * act as a feature group
+ * Returns if a message is a successful response queried by this feature
*
- * @param f the device feature related to this feature
+ * @param msg the message to check
+ * @return true if my direct ack
*/
- public void addConnectedFeature(DeviceFeature f) {
- connectedFeatures.add(f);
+ public boolean isMyDirectAck(Msg msg) {
+ return msg.isAckOfDirect() && !msg.isReplayed() && getQueryStatus() == QueryStatus.QUERY_ACKED;
}
- public boolean hasListeners() {
- if (!listeners.isEmpty()) {
- return true;
- }
- for (DeviceFeature f : connectedFeatures) {
- if (f.hasListeners()) {
- return true;
+ /**
+ * Returns if a message is a failed response queried by this feature
+ *
+ * @param msg the message to check
+ * @return true if my direct nack
+ */
+ public boolean isMyDirectNack(Msg msg) {
+ if (msg.isNackOfDirect() && !msg.isReplayed() && getQueryStatus() == QueryStatus.QUERY_ACKED) {
+ if (logger.isDebugEnabled()) {
+ try {
+ int cmd2 = msg.getInt("command2");
+ if (cmd2 == 0xFF) {
+ logger.debug("got a sender device id not in responder database failed command msg: {}", msg);
+ } else if (cmd2 == 0xFE) {
+ logger.debug("got a no load detected failed command msg: {}", msg);
+ } else if (cmd2 == 0xFD) {
+ logger.debug("got an incorrect checksum failed command msg: {}", msg);
+ } else if (cmd2 == 0xFC) {
+ logger.debug("got a database search timeout failed command msg: {}", msg);
+ } else if (cmd2 == 0xFB) {
+ logger.debug("got an illegal value failed command msg: {}", msg);
+ } else {
+ logger.debug("got an unknown failed command msg: {}", msg);
+ }
+ } catch (FieldException e) {
+ logger.warn("{}:{} error parsing msg {}", device.getAddress(), name, msg, e);
+ }
}
+ return true;
}
return false;
}
/**
- * removes a DeviceFeatureListener from this feature
+ * Returns if a message is a response queried by this feature
*
- * @param aItemName name of the item to remove as listener
- * @return true if a listener was removed
+ * @param msg the message to check
+ * @return true if my direct ack or nack
*/
- public boolean removeListener(String aItemName) {
- boolean listenerRemoved = false;
- synchronized (listeners) {
- for (Iterator it = listeners.iterator(); it.hasNext();) {
- DeviceFeatureListener fl = it.next();
- if (fl.getItemName().equals(aItemName)) {
- it.remove();
- listenerRemoved = true;
- }
- }
- }
- return listenerRemoved;
+ public boolean isMyDirectAckOrNack(Msg msg) {
+ return isMyDirectAck(msg) || isMyDirectNack(msg);
}
- public boolean isReferencedByItem(String aItemName) {
- synchronized (listeners) {
- for (DeviceFeatureListener fl : listeners) {
- if (fl.getItemName().equals(aItemName)) {
- return true;
- }
- }
- }
- return false;
+ /**
+ * Returns if a message is a reply to a query sent by this feature
+ *
+ * @param msg the message to check
+ * @return true if my reply
+ */
+ public boolean isMyReply(Msg msg) {
+ Msg queryMsg = getQueryMessage();
+ return queryMsg != null && msg.isReplyOf(queryMsg) && getQueryStatus() == QueryStatus.QUERY_SENT;
}
/**
- * Called when message is incoming. Dispatches message according to message dispatcher
+ * Handles message according to message dispatcher
*
- * @param msg The message to dispatch
+ * @param msg the message to dispatch
* @return true if dispatch successful
*/
public boolean handleMessage(Msg msg) {
- MessageDispatcher dispatcher = this.dispatcher;
+ MessageDispatcher dispatcher = getMsgDispatcher();
if (dispatcher == null) {
- logger.warn("{} no dispatcher for msg {}", name, msg);
+ logger.warn("{}:{} no dispatcher for msg {}", device.getAddress(), name, msg);
return false;
}
+ logger.trace("{}:{} handling message using dispatcher {}", device.getAddress(), name,
+ dispatcher.getClass().getSimpleName());
return dispatcher.dispatch(msg);
}
/**
- * Called when an openhab command arrives for this device feature
+ * Handles command for this device feature
+ *
+ * @param cmd the command to be executed
+ */
+ public void handleCommand(Command cmd) {
+ handleCommand(new InsteonChannelConfiguration(), cmd);
+ }
+
+ /**
+ * Handles command for this device feature
*
- * @param c the binding config of the item which sends the command
- * @param cmd the command to be exectued
+ * @param config the channel config of the item which sends the command
+ * @param cmd the command to be executed
*/
- public void handleCommand(InsteonChannelConfiguration c, Command cmd) {
- Class extends Command> key = cmd.getClass();
- CommandHandler h = commandHandlers.containsKey(key) ? commandHandlers.get(key) : defaultCommandHandler;
- if (h != null) {
- logger.trace("{} uses {} to handle command {} for {}", getName(), h.getClass().getSimpleName(),
- key.getSimpleName(), getDevice().getAddress());
- h.handleCommand(c, cmd, getDevice());
+ public void handleCommand(InsteonChannelConfiguration config, Command cmd) {
+ String cmdType = cmd.getClass().getSimpleName();
+ CommandHandler cmdHandler = getOrDefaultCommandHandler(cmdType);
+ if (!cmdHandler.canHandle(cmd)) {
+ logger.debug("{}:{} command {}:{} cannot be handled by {}", device.getAddress(), name, cmdType, cmd,
+ cmdHandler.getClass().getSimpleName());
+ return;
}
+ logger.trace("{}:{} handling command {}:{} using handler {}", device.getAddress(), name, cmdType, cmd,
+ cmdHandler.getClass().getSimpleName());
+ cmdHandler.handleCommand(config, cmd);
}
/**
- * Make a poll message using the configured poll message handler
+ * Makes a poll message using the configured poll message handler
*
* @return the poll message
*/
public @Nullable Msg makePollMsg() {
- PollHandler pollHandler = this.pollHandler;
+ PollHandler pollHandler = getPollHandler();
if (pollHandler == null) {
return null;
}
- logger.trace("{} making poll msg for {} using handler {}", getName(), getDevice().getAddress(),
+ logger.trace("{}:{} making poll msg using handler {}", device.getAddress(), name,
pollHandler.getClass().getSimpleName());
- return pollHandler.makeMsg(device);
+ return pollHandler.makeMsg();
}
/**
- * Publish new state to all device feature listeners, but give them
- * additional dataKey and dataValue information so they can decide
- * whether to publish the data to the bus.
+ * Sends request message to device
*
- * @param newState state to be published
- * @param changeType what kind of changes to publish
- * @param dataKey the key on which to filter
- * @param dataValue the value that must be matched
+ * @param msg request message to send
*/
- public void publish(State newState, StateChangeType changeType, String dataKey, String dataValue) {
- logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
- synchronized (listeners) {
- for (DeviceFeatureListener listener : listeners) {
- listener.stateChanged(newState, changeType, dataKey, dataValue);
- }
- }
+ public void sendRequest(Msg msg) {
+ device.sendMessage(msg, this, 0L);
}
/**
- * Publish new state to all device feature listeners
+ * Updates the state for this feature
*
- * @param newState state to be published
- * @param changeType what kind of changes to publish
+ * @param state the state to update
*/
- public void publish(State newState, StateChangeType changeType) {
- logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
- synchronized (listeners) {
- for (DeviceFeatureListener listener : listeners) {
- listener.stateChanged(newState, changeType);
- }
+ public void updateState(State state) {
+ setState(state);
+ listeners.forEach(listener -> listener.stateUpdated(state));
+ }
+
+ /**
+ * Triggers an event this feature
+ *
+ * @param event the event name to trigger
+ */
+ public void triggerEvent(String event) {
+ if (!isEventFeature()) {
+ logger.warn("{}:{} not configured to handle triggered event", device.getAddress(), name);
+ return;
}
+ listeners.forEach(listener -> listener.eventTriggered(event));
}
/**
- * Poll all device feature listeners for related devices
+ * Triggers a poll at this feature, group feature or device level,
+ * in order of precedence depending on pollability
+ *
+ * @param delay scheduling delay (in milliseconds)
*/
- public void pollRelatedDevices() {
- synchronized (listeners) {
- for (DeviceFeatureListener listener : listeners) {
- listener.pollRelatedDevices();
- }
+ public void triggerPoll(long delay) {
+ // determine poll delay for this feature if not provided
+ if (delay == -1) {
+ delay = getPollDelay();
+ }
+ // trigger feature poll if pollable
+ if (doPoll(delay) != null) {
+ logger.trace("{}:{} triggered poll on this feature", device.getAddress(), name);
+ return;
+ }
+ // trigger group feature poll if defined and pollable, as fallback
+ DeviceFeature groupFeature = getGroupFeature();
+ if (groupFeature != null && groupFeature.doPoll(delay) != null) {
+ logger.trace("{}:{} triggered poll on group feature {}", device.getAddress(), name, groupFeature.getName());
+ return;
+ }
+ // trigger device poll limiting to responder features, otherwise
+ if (device instanceof InsteonDevice insteonDevice) {
+ insteonDevice.pollResponders(delay);
}
}
/**
- * Adds a message handler to this device feature.
+ * Returns the poll delay for this feature
*
- * @param cm1 The insteon cmd1 of the incoming message for which the handler should be used
- * @param handler the handler to invoke
+ * @return the poll delay based on device ramp rate if supported and available, otherwise 0
*/
- public void addMessageHandler(int cm1, @Nullable MessageHandler handler) {
- synchronized (msgHandlers) {
- msgHandlers.put(cm1, handler);
+ private long getPollDelay() {
+ if (RampRate.supportsFeatureType(type) && device instanceof InsteonDevice insteonDevice) {
+ State state = insteonDevice.getFeatureState(FEATURE_RAMP_RATE);
+ RampRate rampRate;
+ if (state instanceof QuantityType> rampTime) {
+ rampTime = Objects.requireNonNullElse(rampTime.toInvertibleUnit(Units.SECOND), rampTime);
+ rampRate = RampRate.fromTime(rampTime.doubleValue());
+ } else {
+ rampRate = RampRate.DEFAULT;
+ }
+ return rampRate.getTimeInMilliseconds();
}
+ return 0L;
}
/**
- * Adds a command handler to this device feature
+ * Executes the polling of this feature
*
- * @param c the command for which this handler is invoked
- * @param handler the handler to call
+ * @param delay scheduling delay (in milliseconds)
+ * @return poll message
*/
- public void addCommandHandler(Class extends Command> c, @Nullable CommandHandler handler) {
- synchronized (commandHandlers) {
- commandHandlers.put(c, handler);
+ public @Nullable Msg doPoll(long delay) {
+ Msg msg = makePollMsg();
+ if (msg != null) {
+ device.sendMessage(msg, this, delay);
}
+ return msg;
}
/**
- * Turn DeviceFeature into String
+ * Polls related devices to this feature
+ *
+ * @param delay scheduling delay (in milliseconds)
*/
- @Override
- public String toString() {
- return name + "(" + listeners.size() + ":" + commandHandlers.size() + ":" + msgHandlers.size() + ")";
+ public void pollRelatedDevices(long delay) {
+ if (device instanceof InsteonDevice insteonDevice) {
+ insteonDevice.pollRelatedDevices(getGroup(), delay);
+ }
}
/**
- * Factory method for creating DeviceFeatures.
+ * Polls related devices to a broadcast group
*
- * @param s The name of the device feature to create.
- * @return The newly created DeviceFeature, or null if requested DeviceFeature does not exist.
+ * @param group broadcast group
+ * @param delay scheduling delay (in milliseconds)
*/
- @Nullable
- public static DeviceFeature makeDeviceFeature(String s) {
- DeviceFeature f = null;
- synchronized (features) {
- FeatureTemplate ft = features.get(s);
- if (ft != null) {
- f = ft.build();
- } else {
- logger.warn("unimplemented feature requested: {}", s);
- }
+ public void pollRelatedDevices(int group, long delay) {
+ InsteonModem modem = device instanceof InsteonModem insteonModem ? insteonModem
+ : device instanceof InsteonDevice insteonDevice ? insteonDevice.getModem() : null;
+ if (modem != null) {
+ modem.pollRelatedDevices(group, delay);
}
- return f;
}
/**
- * Reads the features templates from an input stream and puts them in global map
+ * Adjusts related devices to this feature
*
- * @param input the input stream from which to read the feature templates
+ * @param config the channel config
+ * @param cmd the command to adjust to
*/
- public static void readFeatureTemplates(InputStream input) {
- try {
- List featureTemplates = FeatureTemplateLoader.readTemplates(input);
- synchronized (features) {
- for (FeatureTemplate f : featureTemplates) {
- features.put(f.getName(), f);
- }
- }
- } catch (IOException e) {
- logger.warn("IOException while reading device features", e);
- } catch (ParsingException e) {
- logger.warn("Parsing exception while reading device features", e);
+ public void adjustRelatedDevices(InsteonChannelConfiguration config, Command cmd) {
+ if (device instanceof InsteonDevice insteonDevice) {
+ insteonDevice.adjustRelatedDevices(getGroup(), config, cmd);
}
}
/**
- * Reads the feature templates from a file and adds them to a global map
+ * Returns broadcast group for this feature
*
- * @param file name of the file to read from
+ * @param config the channel config
+ * @return the broadcast group if found, otherwise -1
*/
- public static void readFeatureTemplates(String file) {
- try {
- FileInputStream fis = new FileInputStream(file);
- readFeatureTemplates(fis);
- } catch (FileNotFoundException e) {
- logger.warn("cannot read feature templates from file {} ", file, e);
+ public int getBroadcastGroup(InsteonChannelConfiguration config) {
+ if (device instanceof InsteonDevice insteonDevice) {
+ return insteonDevice.getBroadcastGroup(this);
+ } else if (device instanceof InsteonModem) {
+ return config.getGroup();
+ }
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ String s = name + "->" + type;
+ if (!parameters.isEmpty()) {
+ s += parameters;
}
+ s += "(" + commandHandlers.size() + ":" + msgHandlers.size() + ":" + listeners.size() + ")";
+ return s;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ DeviceFeature other = (DeviceFeature) obj;
+ return name.equals(other.name) && type.equals(other.type) && device.equals(other.device);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + name.hashCode();
+ result = prime * result + type.hashCode();
+ result = prime * result + device.hashCode();
+ return result;
}
/**
- * static initializer
+ * Factory method for creating DeviceFeature
+ *
+ * @param device the feature device
+ * @param name the feature name
+ * @param type the feature type
+ * @param parameters the feature parameters
+ * @return the newly created DeviceFeature, or null if requested feature type does not exist.
*/
- static {
- // read features from xml file and store them in a map
- InputStream input = DeviceFeature.class.getResourceAsStream("/device_features.xml");
- Objects.requireNonNull(input);
- readFeatureTemplates(input);
+ public static @Nullable DeviceFeature makeDeviceFeature(Device device, String name, String type,
+ Map parameters) {
+ FeatureTemplate template = FeatureTemplateRegistry.getInstance().getTemplate(type);
+ if (template == null) {
+ return null;
+ }
+
+ DeviceFeature feature = template.build(name, device);
+ feature.addParameters(parameters);
+ feature.initializeQueryStatus();
+
+ return feature;
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceType.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceType.java
index 58f069fd90768..7e08ba400284c 100644
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceType.java
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceType.java
@@ -14,135 +14,123 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.transport.message.FieldException;
+import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
+import org.openhab.binding.insteon.internal.transport.message.Msg;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
/**
- * The DeviceType class holds device type definitions that are read from
- * an xml file.
+ * The {@link DeviceType} represents a device type
*
* @author Bernd Pfrommer - Initial contribution
* @author Rob Nielsen - Port to openHAB 2 insteon binding
+ * @author Jeremy Setton - Rewrite insteon binding
*/
@NonNullByDefault
public class DeviceType {
- private String productKey;
- private String model = "";
- private String description = "";
- private Map features = new HashMap<>();
- private Map featureGroups = new HashMap<>();
+ private String name;
+ private Map flags = new HashMap<>();
+ private Map features = new LinkedHashMap<>();
+ private Map links = new LinkedHashMap<>();
/**
* Constructor
*
- * @param aProductKey the product key for this device type
+ * @param name the name for this device type
+ * @param flags the flags for this device type
+ * @param features the features for this device type
+ * @param links the default links for this device type
*/
- public DeviceType(String aProductKey) {
- productKey = aProductKey;
+ public DeviceType(String name, Map flags, Map features,
+ Map links) {
+ this.name = name;
+ this.flags = flags;
+ this.features = features;
+ this.links = links;
}
/**
- * Get supported features
+ * Returns name
*
- * @return all features that this device type supports
- */
- public Map getFeatures() {
- return features;
- }
-
- /**
- * Get all feature groups
- *
- * @return all feature groups of this device type
+ * @return the name for this device type
*/
- public Map getFeatureGroups() {
- return featureGroups;
+ public String getName() {
+ return name;
}
/**
- * Sets the descriptive model string
+ * Returns flags
*
- * @param aModel descriptive model string
+ * @return all flags for this device type
*/
- public void setModel(String aModel) {
- model = aModel;
+ public Map getFlags() {
+ return flags;
}
/**
- * Sets free text description
+ * Returns supported features
*
- * @param aDesc free text description
+ * @return all features that this device type supports
*/
- public void setDescription(String aDesc) {
- description = aDesc;
+ public List getFeatures() {
+ return features.values().stream().toList();
}
/**
- * Adds feature to this device type
+ * Returns supported feature groups
*
- * @param aKey the key (e.g. "switch") under which this feature can be referenced in the item binding config
- * @param aFeatureName the name (e.g. "GenericSwitch") under which the feature has been defined
- * @return false if feature was already there
+ * @return all feature groups that this device type supports
*/
- public boolean addFeature(String aKey, String aFeatureName) {
- if (features.containsKey(aKey)) {
- return false;
- }
- features.put(aKey, aFeatureName);
- return true;
+ public List getFeatureGroups() {
+ return features.values().stream().filter(FeatureEntry::hasConnectedFeatures).toList();
}
/**
- * Adds feature group to device type
+ * Returns default links
*
- * @param aKey name of the feature group, which acts as key for lookup later
- * @param fg feature group to add
- * @return true if add succeeded, false if group was already there
+ * @return all default links for this device type
*/
- public boolean addFeatureGroup(String aKey, FeatureGroup fg) {
- if (features.containsKey(aKey)) {
- return false;
- }
- featureGroups.put(aKey, fg);
- return true;
+ public Map getDefaultLinks() {
+ return links;
}
@Override
public String toString() {
- String s = "pk:" + productKey + "|model:" + model + "|desc:" + description + "|features";
- for (Entry f : features.entrySet()) {
- s += ":" + f.getKey() + "=" + f.getValue();
+ String s = "name:" + name;
+ if (!features.isEmpty()) {
+ s += "|features:" + features.values().stream().map(FeatureEntry::toString).collect(Collectors.joining(","));
+ }
+ if (!flags.isEmpty()) {
+ s += "|flags:" + flags.entrySet().stream().map(Entry::toString).collect(Collectors.joining(","));
}
- s += "|groups";
- for (Entry f : featureGroups.entrySet()) {
- s += ":" + f.getKey() + "=" + f.getValue();
+ if (!links.isEmpty()) {
+ s += "|default-links:"
+ + links.values().stream().map(DefaultLinkEntry::toString).collect(Collectors.joining(","));
}
return s;
}
/**
- * Class that reflects feature group association
- *
- * @author Bernd Pfrommer - Initial contribution
+ * Class that reflects a feature entry
*/
- public static class FeatureGroup {
+ public static class FeatureEntry {
private String name;
private String type;
- private ArrayList fgFeatures = new ArrayList<>();
+ private Map parameters;
+ private List connectedFeatures = new ArrayList<>();
- FeatureGroup(String name, String type) {
+ public FeatureEntry(String name, String type, Map parameters) {
this.name = name;
this.type = type;
- }
-
- public void addFeature(String f) {
- fgFeatures.add(f);
- }
-
- public ArrayList getFeatures() {
- return fgFeatures;
+ this.parameters = parameters;
}
public String getName() {
@@ -153,13 +141,127 @@ public String getType() {
return type;
}
+ public Map getParameters() {
+ return parameters;
+ }
+
+ public List getConnectedFeatures() {
+ return connectedFeatures;
+ }
+
+ public boolean hasConnectedFeatures() {
+ return !connectedFeatures.isEmpty();
+ }
+
+ public void addConnectedFeature(String name) {
+ connectedFeatures.add(name);
+ }
+
@Override
public String toString() {
- String s = "";
- for (String g : fgFeatures) {
- s += g + ",";
+ String s = name + "->" + type;
+ if (!connectedFeatures.isEmpty()) {
+ s += "|connectedFeatures:" + connectedFeatures;
}
- return (s.replaceAll(",$", ""));
+ return s;
+ }
+ }
+
+ /**
+ * Class that reflects a default link entry
+ */
+ public static class DefaultLinkEntry {
+ private String name;
+ private boolean controller;
+ private int group;
+ private byte[] data;
+ private List commands = new ArrayList<>();
+
+ public DefaultLinkEntry(String name, boolean controller, int group, byte[] data) {
+ this.name = name;
+ this.controller = controller;
+ this.group = group;
+ this.data = data;
+ }
+
+ public boolean isController() {
+ return controller;
+ }
+
+ public int getGroup() {
+ return group;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public List getCommands() {
+ return commands;
+ }
+
+ public void addCommand(CommandEntry command) {
+ commands.add(command);
+ }
+
+ @Override
+ public String toString() {
+ String s = name + "->";
+ s += controller ? "CTRL" : "RESP";
+ s += "|group:" + group;
+ s += "|data1:" + HexUtils.getHexString(data[0]);
+ s += "|data2:" + HexUtils.getHexString(data[1]);
+ s += "|data3:" + HexUtils.getHexString(data[2]);
+ if (!commands.isEmpty()) {
+ s += "|commands:" + commands;
+ }
+ return s;
+ }
+ }
+
+ /**
+ * Class that reflects a command entry
+ */
+ public static class CommandEntry {
+ private String name;
+ private int ext;
+ private byte cmd1;
+ private byte cmd2;
+ private byte[] data;
+
+ public CommandEntry(String name, int ext, byte cmd1, byte cmd2, byte[] data) {
+ this.name = name;
+ this.ext = ext;
+ this.cmd1 = cmd1;
+ this.cmd2 = cmd2;
+ this.data = data;
+ }
+
+ public @Nullable Msg getMessage(InsteonDevice device) {
+ try {
+ if (ext == 0) {
+ return Msg.makeStandardMessage(device.getAddress(), cmd1, cmd2);
+ } else if (ext == 1) {
+ return Msg.makeExtendedMessage(device.getAddress(), cmd1, cmd2, data,
+ device.getInsteonEngine().supportsChecksum());
+ } else if (ext == 2) {
+ return Msg.makeExtendedMessageCRC2(device.getAddress(), cmd1, cmd2, data);
+ }
+ } catch (FieldException | InvalidMessageTypeException e) {
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ String s = name + "->";
+ s += "ext:" + ext;
+ s += "|cmd1:" + HexUtils.getHexString(cmd1);
+ s += "|cmd2:" + HexUtils.getHexString(cmd2);
+ s += "|data1:" + HexUtils.getHexString(data[0]);
+ s += "|data2:" + HexUtils.getHexString(data[1]);
+ s += "|data3:" + HexUtils.getHexString(data[2]);
+ return s;
}
}
}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeLoader.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeLoader.java
deleted file mode 100644
index 52af9488ef465..0000000000000
--- a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeLoader.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.insteon.internal.device;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.insteon.internal.device.DeviceType.FeatureGroup;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-
-/**
- * Reads the device types from an xml file.
- *
- * @author Daniel Pfrommer - Initial contribution
- * @author Bernd Pfrommer - openHAB 1 insteonplm binding
- * @author Rob Nielsen - Port to openHAB 2 insteon binding
- */
-@NonNullByDefault
-public class DeviceTypeLoader {
- private static final Logger logger = LoggerFactory.getLogger(DeviceTypeLoader.class);
- private Map deviceTypes = new HashMap<>();
- private static DeviceTypeLoader deviceTypeLoader = new DeviceTypeLoader();
-
- private DeviceTypeLoader() {
- } // private so nobody can call it
-
- /**
- * Finds the device type for a given product key
- *
- * @param aProdKey product key to search for
- * @return the device type, or null if not found
- */
- public @Nullable DeviceType getDeviceType(String aProdKey) {
- return (deviceTypes.get(aProdKey));
- }
-
- /**
- * Must call loadDeviceTypesXML() before calling this function!
- *
- * @return currently known device types
- */
- public Map getDeviceTypes() {
- return (deviceTypes);
- }
-
- /**
- * Reads the device types from input stream and stores them in memory for
- * later access.
- *
- * @param in the input stream from which to read
- */
- public void loadDeviceTypesXML(InputStream in) throws ParserConfigurationException, SAXException, IOException {
- DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
- // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- dbFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
- dbFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
- dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
- dbFactory.setXIncludeAware(false);
- dbFactory.setExpandEntityReferences(false);
- DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
- Document doc = dBuilder.parse(in);
- doc.getDocumentElement().normalize();
- Node root = doc.getDocumentElement();
- NodeList nodes = root.getChildNodes();
- for (int i = 0; i < nodes.getLength(); i++) {
- Node node = nodes.item(i);
- if (node.getNodeType() == Node.ELEMENT_NODE && "device".equals(node.getNodeName())) {
- processDevice((Element) node);
- }
- }
- }
-
- /**
- * Reads the device types from file and stores them in memory for later access.
- *
- * @param aFileName The name of the file to read from
- * @throws ParserConfigurationException
- * @throws SAXException
- * @throws IOException
- */
- public void loadDeviceTypesXML(String aFileName) throws ParserConfigurationException, SAXException, IOException {
- File file = new File(aFileName);
- InputStream in = new FileInputStream(file);
- loadDeviceTypesXML(in);
- }
-
- /**
- * Process device node
- *
- * @param e name of the element to process
- * @throws SAXException
- */
- private void processDevice(Element e) throws SAXException {
- String productKey = e.getAttribute("productKey");
- if ("".equals(productKey)) {
- throw new SAXException("device in device_types file has no product key!");
- }
- if (deviceTypes.containsKey(productKey)) {
- logger.warn("overwriting previous definition of device {}", productKey);
- deviceTypes.remove(productKey);
- }
- DeviceType devType = new DeviceType(productKey);
-
- NodeList nodes = e.getChildNodes();
- for (int i = 0; i < nodes.getLength(); i++) {
- Node node = nodes.item(i);
- if (node.getNodeType() != Node.ELEMENT_NODE) {
- continue;
- }
- Element subElement = (Element) node;
- String nodeName = subElement.getNodeName();
- if ("model".equals(nodeName)) {
- devType.setModel(subElement.getTextContent());
- } else if ("description".equals(nodeName)) {
- devType.setDescription(subElement.getTextContent());
- } else if ("feature".equals(nodeName)) {
- processFeature(devType, subElement);
- } else if ("feature_group".equals(nodeName)) {
- processFeatureGroup(devType, subElement);
- }
- deviceTypes.put(productKey, devType);
- }
- }
-
- private String processFeature(DeviceType devType, Element e) throws SAXException {
- String name = e.getAttribute("name");
- if ("".equals(name)) {
- throw new SAXException("feature " + e.getNodeName() + " has feature without name!");
- }
- if (!name.equals(name.toLowerCase())) {
- throw new SAXException("feature name '" + name + "' must be lower case");
- }
- if (!devType.addFeature(name, e.getTextContent())) {
- throw new SAXException("duplicate feature: " + name);
- }
- return (name);
- }
-
- private String processFeatureGroup(DeviceType devType, Element e) throws SAXException {
- String name = e.getAttribute("name");
- if ("".equals(name)) {
- throw new SAXException("feature group " + e.getNodeName() + " has no name attr!");
- }
- String type = e.getAttribute("type");
- if ("".equals(type)) {
- throw new SAXException("feature group " + e.getNodeName() + " has no type attr!");
- }
- FeatureGroup fg = new FeatureGroup(name, type);
- NodeList nodes = e.getChildNodes();
- for (int i = 0; i < nodes.getLength(); i++) {
- Node node = nodes.item(i);
- if (node.getNodeType() != Node.ELEMENT_NODE) {
- continue;
- }
- Element subElement = (Element) node;
- String nodeName = subElement.getNodeName();
- if ("feature".equals(nodeName)) {
- fg.addFeature(processFeature(devType, subElement));
- } else if ("feature_group".equals(nodeName)) {
- fg.addFeature(processFeatureGroup(devType, subElement));
- }
- }
- if (!devType.addFeatureGroup(name, fg)) {
- throw new SAXException("duplicate feature group " + name);
- }
- return (name);
- }
-
- /**
- * Singleton instance function, creates DeviceTypeLoader
- *
- * @return DeviceTypeLoader singleton reference
- */
- @Nullable
- public static synchronized DeviceTypeLoader instance() {
- if (deviceTypeLoader.getDeviceTypes().isEmpty()) {
- InputStream input = DeviceTypeLoader.class.getResourceAsStream("/device_types.xml");
- try {
- if (input != null) {
- deviceTypeLoader.loadDeviceTypesXML(input);
- } else {
- logger.warn("Resource stream is null, cannot read xml file.");
- }
- } catch (ParserConfigurationException e) {
- logger.warn("parser config error when reading device types xml file: ", e);
- } catch (SAXException e) {
- logger.warn("SAX exception when reading device types xml file: ", e);
- } catch (IOException e) {
- logger.warn("I/O exception when reading device types xml file: ", e);
- }
- }
- return deviceTypeLoader;
- }
-}
diff --git a/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeRegistry.java b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeRegistry.java
new file mode 100644
index 0000000000000..046f878d48b2c
--- /dev/null
+++ b/bundles/org.openhab.binding.insteon/src/main/java/org/openhab/binding/insteon/internal/device/DeviceTypeRegistry.java
@@ -0,0 +1,309 @@
+/**
+ * Copyright (c) 2010-2024 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.insteon.internal.device;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.insteon.internal.InsteonResourceLoader;
+import org.openhab.binding.insteon.internal.device.DeviceType.CommandEntry;
+import org.openhab.binding.insteon.internal.device.DeviceType.DefaultLinkEntry;
+import org.openhab.binding.insteon.internal.device.DeviceType.FeatureEntry;
+import org.openhab.binding.insteon.internal.utils.HexUtils;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * The {@link DeviceTypeRegistry} represents the device type registry
+ *
+ * @author Daniel Pfrommer - Initial contribution
+ * @author Bernd Pfrommer - openHAB 1 insteonplm binding
+ * @author Rob Nielsen - Port to openHAB 2 insteon binding
+ * @author Jeremy Setton - Rewrite insteon binding
+ */
+@NonNullByDefault
+public class DeviceTypeRegistry extends InsteonResourceLoader {
+ private static final DeviceTypeRegistry DEVICE_TYPE_REGISTRY = new DeviceTypeRegistry();
+ private static final String RESOURCE_NAME = "/device-types.xml";
+
+ private Map deviceTypes = new LinkedHashMap<>();
+ private Map