diff --git a/CODEOWNERS b/CODEOWNERS index a6d6e160d38..903e746634a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -111,6 +111,7 @@ /bundles/org.openhab.binding.folding/ @fa2k /bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand /bundles/org.openhab.binding.freebox/ @lolodomo +/bundles/org.openhab.binding.freeboxos/ @clinique /bundles/org.openhab.binding.fronius/ @trokohl /bundles/org.openhab.binding.fsinternetradio/ @paphko /bundles/org.openhab.binding.ftpupload/ @paulianttila diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 2a48a1987d3..61c86ec949d 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -551,6 +551,11 @@ org.openhab.binding.freebox ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.freeboxos + ${project.version} + org.openhab.addons.bundles org.openhab.binding.fronius diff --git a/bundles/org.openhab.binding.freeboxos/NOTICE b/bundles/org.openhab.binding.freeboxos/NOTICE new file mode 100644 index 00000000000..89286014860 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/NOTICE @@ -0,0 +1,20 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons + +== Third-party Content + +IPAddress: Java library for handling IP addresses and subnets, both IPv4 and IPv6 +* License: Apache License 2.0 +* Project: https://github.com/seancfoley/IPAddress +* Source: https://github.com/seancfoley/IPAddress diff --git a/bundles/org.openhab.binding.freeboxos/README.md b/bundles/org.openhab.binding.freeboxos/README.md new file mode 100644 index 00000000000..5c4c8ace990 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/README.md @@ -0,0 +1,210 @@ +# FreeboxOS Binding + +Free is a French telecom operator providing advanced equipments to manage your broadband access. + +This binding integrates the [Freebox Revolution](https://www.free.fr/freebox/freebox-revolution/) and [Freebox Delta](https://www.free.fr/freebox/freebox-delta/) to your openHAB installation. + +The server can be connected to Free infrastructures either by xDSL or fiber optic. + +This binding provides metrics about your network bridge/router and attached devices (wifi repeaters, TV boxes ...). +It also provides home automation capabilities when appropriate dongle has been inserted in the server. + +IliadBox, italian version of the Freebox Pop are also compatible. + +## Supported Things + +This binding supports the following thing types: + +| Thing | Thing Type | Description | +|-------------------|------------|---------------------------------------------------------------| +| api | Bridge | Bridge to access freebox OS API hosted by the server | +| delta | Thing | A Freebox Delta server | +| revolution | Thing | A Freebox Revolution server | +| player | Thing | A TV player equipment | +| active-player | Thing | The TV player equipment with API capabilities (e.g. Devialet) | +| landline | Thing | The phone wired to the Freebox Server | +| host | Thing | A network device on the local network | +| wifihost | Thing | A wifi networked device on the local network | +| vm (*) | Thing | A virtual machine hosted on the server | +| freeplug | Thing | A virtual machine hosted on the server | +| repeater | Thing | A Free wifi repeater | +| basic-shutter (*) | Thing | RTS Shutter configured in Freebox Home | +| shutter (*) | Thing | IO Home Control shutter configured in Freebox Home | +| kfb (*) | Thing | A keyfob associated with your alarm system | +| alarm (*) | Thing | The Freebox Home Alarm System | + +(*) Restricted to servers >= Delta + +## Discovery + +The API bridge is discovered automatically through mDNS in the local network. +After the bridge is discovered and available to openHAB, the binding will automatically discover phone, network devices available on the local network. +Note that the discovered thing will be setup to use only HTTP API (and not HTTPS) because only an IP can be automatically determined while a domain name is required to use HTTPS with a valid certificate. + +## Binding configuration + +FreeboxOS binding has the following configuration parameters: + +| Name | Description | Mandatory | +|-----------------|----------------------------------------------------|-----------| +| timeout | The timeout for reading from the device in seconds | yes | + + +## Thing Configuration + +### API bridge + +| Parameter Label | Parameter ID | Description | Required | Default | +|--------------------------|-------------------|--------------------------------------------------------|----------|----------------------| +| Freebox Server Address | apiDomain | The domain to use in place of hardcoded Freebox ip | No | mafreebox.freebox.fr | +| Application Token | appToken | Token generated by the Freebox Server. | Yes | | +| Network Device Discovery | discoverNetDevice | Enable the discovery of network device things. | No | false | +| HTTPS Available | httpsAvailable | Tells if https has been configured on the Freebox | No | false | +| HTTPS port | httpsPort | Port to use for remote https access to the Freebox Api | No | 15682 | + +If the parameter *apiDomain* is not set, the binding will use the default address used by Free to access your Freebox Server (mafreebox.freebox.fr). +The bridge thing will initialize only if a valid application token (parameter *appToken*) is filled. + +### Server: Revolution or Delta + +The *revolution* or *delta* thing requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|--------------------------------------------------------------------------|----------|---------| +| Refresh Interval | refreshInterval | The refresh interval (seconds) which is used to poll the Freebox Server. | No | 30 | + +### Player thing + +The *player* thing requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|----------------------------------------------------------------------------|----------|---------| +| MAC Address | macAddress | The MAC address of the player device. | Yes | | +| ID | id | Id of the player within Freebox Api | Yes | 1 | +| Player port | port | | No | 24322 | +| Password | password | AirPlay password | No | | +| Remote Code | remoteCode | Code associated to remote control | No | | +| Accept all MP3 | acceptAllMp3 | Accept any bitrate for MP3 audio or only bitrates greater than 64 kbps | No | true | +| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll the player | Yes | 30 | +| Callback URL | callbackUrl | URL to use for playing notification sounds, e.g. 'http://192.168.0.2:8080' | No | | + +### Landline + +The *landline* thing requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|------------------------------------------------------------------------|----------|---------| +| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll for phone state. | No | 2 | + +### Network devices: Host and WifiHost + +The *host* and *wifihost* things requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|------------------------------------------------------------------------|----------|---------| +| MAC Address | macAddress | The MAC address of the network host . | Yes | | +| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll for phone state. | No | 30 | + +### Repeater and Vm thing + +The *repeater* thing is a specialized case of a *wifihost*. The *vm* derives from *host*. They share the same configuration definition: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|------------------------------------------------------------------|----------|---------| +| MAC Address | macAddress | The MAC address of the player device. | No | | +| ID | id | Id of the repeater within Freebox Api | Yes | 1 | +| Refresh Interval | refreshInterval | The refresh interval in seconds which is used to poll the player | Yes | 30 | + +### Basic shutter thing + +The *basic-shutter* thing requires the following configuration parameters: + +| Parameter Label | Parameter ID | Description | Required | Default | +|------------------|-----------------|------------------------------------------------------------------|----------|---------| +| ID | id | Id of the Home Node | Yes | 1 | +| UP Slot ID | upSlotId | Id of the UP Slot. | Yes | 0 | +| STOP Slot ID | stopSlotId | Id of the STOP Slot. | Yes | 1 | +| DOWN Slot ID | downSlotId | Id of the DOWN Slot. | Yes | 2 | +| STATE Signal ID | stateSignalId | Id of the STATE Signal. | Yes | 3 | + +## Authentication + +You will have to authorize openHAB to connect to your Freebox. Here is the process described: + +**Step 1** At binding startup, if no token is recorded in the Freebox Server (bridge) configuration, the binding will run a pairing request + +**Step 2** Run to your Freebox and approve the pairing request for openHAB FreeboxOS Binding that is displayed on the Freebox screen + +**Step 3** the application token is automatically recorded in the Freebox Server (bridge) configuration + +**Step 4** you can use the console command `freeboxos apptoken` to display the application token and use it later to set up your thing in a configuration file + +**Step 5** log in your Freebox admin console to allocate needed rights to FreeboxOS Binding + +Once initialized, the thing will generate all available channels. + +## Channels + +The following channels are supported: + +| Thing | Channel Type ID | Item Type | Access Mode | Description | +|---------------|-----------------------------|---------------|-------------|--------------------------------------------------------------------------------| +| revolution | lcd-brightness | Number | RW | Brightness level of the screen in percent | +| revolution | lcd-orientation | Number | RW | Screen Orientation in degrees (0 or 90 or 180 or 270) | +| revolution | lcd-forced | Switch | RW | Indicates whether the screen orientation forced | +| server (*) | uptime | Number | R | Time since last reboot of the Freebox Server | +| server (*) | restarted | Switch | R | Indicates whether the Freebox server has restarted during the last poll period | +| server (*) | wifi-status | Switch | RW | Indicates whether the WiFi network is enabled | +| server (*) | ftp-status | Switch | RW | Indicates whether the FTP server is enabled | +| server (*) | airmedia-status | Switch | RW | Indicates whether Air Media is enabled | +| server (*) | upnpav-status | Switch | RW | Indicates whether UPnP AV is enabled | +| server (*) | samba-file-status | Switch | RW | Indicates whether Window File Sharing is enabled | +| server (*) | samba-printer-status | Switch | RW | Indicates whether Window Printer Sharing is enabled | +| server (*) | xdsl-status | String | R | Status of the xDSL line | +| server (*) | ftth-status | Switch | R | Status of the Ftth line | +| server (*) | line-status | String | R | Status of network line connexion | +| server (*) | ipv4 | String | R | Public IP Address of the Freebox Server | +| server (*) | rate-up | Number | R | Current upload rate in byte/s | +| server (*) | rate-down | Number | R | Current download rate in byte/s | +| server (*) | bytes-up | Number | R | Total uploaded bytes since last connection | +| server (*) | bytes-down | Number | R | Total downloaded bytes since last connection | +| phone | state#onhook | Switch | R | Indicates whether the phone is on hook | +| phone | state#ringing | Switch | R | Is the phone ringing | +| call | incoming#number | Call | R | Current incoming call number | +| call | incoming#timestamp | DateTime | R | Current incoming call creation timestamp | +| call | incoming#name | String | R | Current incoming caller name | +| call | accepted#number | Call | R | Last accepted call number | +| call | accepted#duration | Number | R | Last accepted call duration in seconds | +| call | accepted#timestamp | DateTime | R | Last accepted call creation timestamp | +| call | accepted#name | String | R | Last accepted caller name | +| call | missed#number | Call | R | Last missed call number | +| call | missed#timestamp | DateTime | R | Last missed call creation timestamp | +| call | missed#name | String | R | Last missed caller name | +| call | outgoing#number | Call | R | Last outgoing call number | +| call | outgoing#duration | Number | R | Last outgoing call duration in seconds | +| call | outgoing#timestamp | DateTime | R | Last outgoing call creation timestamp | +| call | outgoing#name | String | R | Last outgoing called name | +| net_device | reachable | Switch | R | Indicates whether the network device is reachable | +| net_interface | reachable | Switch | R | Indicates whether the network interface is reachable | +| airplay | playurl | String | W | Play an audio or video media from the given URL | +| airplay | stop | Switch | W | Stop the media playback | +| basic-shutter | basic-shutter#basic-shutter | RollerShutter | W | Up, stop and down commands for a RTS shutter | + +(*): server means *delta* or *revolution* + +## Actions for rules + +The following actions are available in rules/scripting: + +| Thing Type | Action Name | Parameters | Description | +|-------------|------------------|-------------------------|------------------------------------------------------| +| host | wol | None | Sends a wake on lan packet to the lan connected host | +| player | reboot | None | Reboots the player device | +| player | sendKey | key: String | Send a key (remote emulation) to the player | +| player | sendLongKey | key: String | Sends the key emulating a longpress on the button | +| player | sendMultipleKeys | keys: String | Sends multiple keys to the player, comma separated | +| player | sendKeyRepeat | key: String, count: int | Sends the key multiple times | +| server | reboot | None | Reboots the Freebox Server | +| freeplug | reset | None | Resets the Freeplug | +| call | reset | None | Clears the call log queue | +| repeater | reboot | None | Reboots the Repeater | diff --git a/bundles/org.openhab.binding.freeboxos/pom.xml b/bundles/org.openhab.binding.freeboxos/pom.xml new file mode 100644 index 00000000000..f28ee769b12 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.freeboxos + + openHAB Add-ons :: Bundles :: FreeboxOS Binding + + + + com.google.code.gson + gson + 2.10.1 + + + com.github.seancfoley + ipaddress + 5.4.0 + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/feature/feature.xml b/bundles/org.openhab.binding.freeboxos/src/main/feature/feature.xml new file mode 100644 index 00000000000..bb55dd198ab --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-mdns + mvn:org.openhab.addons.bundles/org.openhab.binding.freeboxos/${project.version} + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsBindingConstants.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsBindingConstants.java new file mode 100644 index 00000000000..88c71f283de --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsBindingConstants.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Category; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.types.Command; + +/** + * The {@link FreeboxBinding} class defines common constants, which are used across the binding. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxOsBindingConstants { + + public static final String BINDING_ID = "freeboxos"; + + // List of all Bridge Type UIDs + public static final ThingTypeUID BRIDGE_TYPE_API = new ThingTypeUID(BINDING_ID, "api"); + + // Thing Types ID strings + private static final String THING_DECT = "dect"; + private static final String THING_FXS = "fxs"; + private static final String THING_REVOLUTION = "revolution"; + private static final String THING_DELTA = "delta"; + private static final String THING_WIFI_HOST = "wifihost"; + private static final String THING_ACTIVE_PLAYER = "active-player"; + + public static final String THING_FREEPLUG = "freeplug"; + public static final String THING_VM = "vm"; + public static final String THING_CALL = "call"; + public static final String THING_HOST = "host"; + public static final String THING_PLAYER = "player"; + public static final String THING_REPEATER = "repeater"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_REVOLUTION = new ThingTypeUID(BINDING_ID, THING_REVOLUTION); + public static final ThingTypeUID THING_TYPE_DELTA = new ThingTypeUID(BINDING_ID, THING_DELTA); + public static final ThingTypeUID THING_TYPE_FXS = new ThingTypeUID(BINDING_ID, THING_FXS); + public static final ThingTypeUID THING_TYPE_DECT = new ThingTypeUID(BINDING_ID, THING_DECT); + public static final ThingTypeUID THING_TYPE_CALL = new ThingTypeUID(BINDING_ID, THING_CALL); + public static final ThingTypeUID THING_TYPE_FREEPLUG = new ThingTypeUID(BINDING_ID, THING_FREEPLUG); + public static final ThingTypeUID THING_TYPE_HOST = new ThingTypeUID(BINDING_ID, THING_HOST); + public static final ThingTypeUID THING_TYPE_WIFI_HOST = new ThingTypeUID(BINDING_ID, THING_WIFI_HOST); + public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, THING_PLAYER); + public static final ThingTypeUID THING_TYPE_ACTIVE_PLAYER = new ThingTypeUID(BINDING_ID, THING_ACTIVE_PLAYER); + public static final ThingTypeUID THING_TYPE_VM = new ThingTypeUID(BINDING_ID, THING_VM); + public static final ThingTypeUID THING_TYPE_REPEATER = new ThingTypeUID(BINDING_ID, THING_REPEATER); + + // All supported Thing types + public static final Set BRIDGE_TYPE_UIDS = Set.of(BRIDGE_TYPE_API); + public static final Set THINGS_TYPES_UIDS = Set.of(THING_TYPE_FXS, THING_TYPE_DECT, THING_TYPE_CALL, + THING_TYPE_HOST, THING_TYPE_VM, THING_TYPE_PLAYER, THING_TYPE_ACTIVE_PLAYER, THING_TYPE_DELTA, + THING_TYPE_REVOLUTION, THING_TYPE_REPEATER, THING_TYPE_WIFI_HOST, THING_TYPE_FREEPLUG); + public static final Set HOME_TYPES_UIDS = Set.of(Category.BASIC_SHUTTER.getThingTypeUID(), + Category.SHUTTER.getThingTypeUID(), Category.KFB.getThingTypeUID(), Category.CAMERA.getThingTypeUID(), + Category.ALARM.getThingTypeUID()); + + protected static final Set SUPPORTED_THING_TYPES_UIDS = Stream + .of(BRIDGE_TYPE_UIDS, THINGS_TYPES_UIDS, HOME_TYPES_UIDS).flatMap(Set::stream).collect(Collectors.toSet()); + + // Thing properties + // public static final String LAST_CALL_TIMESTAMP = "lastCallTimestamp"; + public static final String ROLE = "role"; + public static final String NET_ID = "netId"; + public static final String ETHERNET_SPEED = "ethernetSpeed"; + public static final String LOCAL = "local"; + public static final String FULL_DUPLEX = "fullDuplex"; + + // List of all Group Channel ids + public static final String GROUP_SENSORS = "sensors"; + public static final String GROUP_FANS = "fans"; + public static final String CONNECTION_STATUS = "connection-status"; + public static final String SYS_INFO = "sysinfo"; + public static final String ACTIONS = "actions"; + public static final String FILE_SHARING = "file-sharing"; + public static final String CONNECTIVITY = "connectivity"; + public static final String DISPLAY = "display"; + public static final String VM_STATUS = "vmstatus"; + public static final String GROUP_WIFI = "wifi"; + public static final String REPEATER_MISC = "repeater-misc"; + + // List of all Channel ids + public static final String RSSI = "rssi"; + public static final String SSID = "ssid"; + public static final String WIFI_QUALITY = "wifi-quality"; + public static final String WIFI_HOST = "wifi-host"; + public static final String UPTIME = "uptime"; + public static final String BOX_EVENT = "box-event"; + public static final String LCD_BRIGHTNESS = "lcd-brightness"; + public static final String LCD_ORIENTATION = "lcd-orientation"; + public static final String LCD_FORCED = "lcd-forced"; + public static final String WIFI_STATUS = "wifi-status"; + public static final String IP_ADDRESS = "ip-address"; + public static final String IPV6_ADDRESS = "ipv6-address"; + public static final String LINE_STATUS = "line-status"; + public static final String LINE_TYPE = "line-type"; + public static final String LINE_MEDIA = "line-media"; + public static final String PLAYER_STATUS = "player-status"; + public static final String PACKAGE = "package"; + public static final String RATE = "rate"; + public static final String BYTES_UP = "bytes-up"; + public static final String BYTES_DOWN = "bytes-down"; + public static final String BW = "bandwidth"; + public static final String PCT_BW = "bandwidth-usage"; + public static final String ONHOOK = "onhook"; + public static final String RINGING = "ringing"; + public static final String HARDWARE_STATUS = "hardware-status"; + public static final String TELEPHONY_SERVICE = "telephony-service"; + public static final String GAIN_RX = "gain-rx"; + public static final String GAIN_TX = "gain-tx"; + public static final String FTP_STATUS = "ftp-status"; + public static final String SAMBA_FILE_STATUS = "samba-file-status"; + public static final String SAMBA_PRINTER_STATUS = "samba-printer-status"; + public static final String AFP_FILE_STATUS = "afp-file-status"; + public static final String REACHABLE = "reachable"; + public static final String LAST_SEEN = "last-seen"; + public static final String ALTERNATE_RING = "lcd-forced"; + public static final String DECT_ACTIVE = "dect-active"; + public static final String UPNPAV_STATUS = "upnpav-status"; + + // Call channels for groups Accepted, Missed and Outgoing + public static final String NUMBER = "number"; + public static final String DURATION = "duration"; + public static final String TIMESTAMP = "timestamp"; + public static final String NAME = "name"; + + // Freebox player channels + public static final String AIRMEDIA_STATUS = "airmedia-status"; + public static final String KEY_CODE = "key-code"; + + // Virtual machine channels + public static final String STATUS = "status"; + + // Repeater channels + public static final String LED = "led"; + public static final String HOST_COUNT = "host-count"; + + // Home channels + public static final String KEYFOB_ENABLE = "enable"; + public static final String NODE_BATTERY = "battery"; + public static final String SHUTTER_POSITION = "position-set"; + public static final String SHUTTER_STOP = "stop"; + public static final String BASIC_SHUTTER_STATE = "state"; + public static final String BASIC_SHUTTER_UP = "up"; + public static final String BASIC_SHUTTER_DOWN = "down"; + // public static final String BASIC_SHUTTER_CMD = "basic-shutter"; + public static final String ALARM_PIN = "pin"; + public static final String ALARM_SOUND = "sound"; + public static final String ALARM_VOLUME = "volume"; + public static final String ALARM_TIMEOUT1 = "timeout1"; + public static final String ALARM_TIMEOUT2 = "timeout2"; + public static final String ALARM_TIMEOUT3 = "timeout3"; + + public static final Set TRUE_COMMANDS = Set.of(OnOffType.ON, UpDownType.UP, OpenClosedType.OPEN); + public static final Set> ON_OFF_CLASSES = Set.of(OnOffType.class, UpDownType.class, OpenClosedType.class); +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsHandlerFactory.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsHandlerFactory.java new file mode 100644 index 00000000000..f835aa29572 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/FreeboxOsHandlerFactory.java @@ -0,0 +1,166 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.freeboxos.internal.api.ApiHandler; +import org.openhab.binding.freeboxos.internal.api.rest.FreeboxOsSession; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Category; +import org.openhab.binding.freeboxos.internal.handler.ActivePlayerHandler; +import org.openhab.binding.freeboxos.internal.handler.AlarmHandler; +import org.openhab.binding.freeboxos.internal.handler.BasicShutterHandler; +import org.openhab.binding.freeboxos.internal.handler.CallHandler; +import org.openhab.binding.freeboxos.internal.handler.CameraHandler; +import org.openhab.binding.freeboxos.internal.handler.DectHandler; +import org.openhab.binding.freeboxos.internal.handler.FreeboxOsHandler; +import org.openhab.binding.freeboxos.internal.handler.FreeplugHandler; +import org.openhab.binding.freeboxos.internal.handler.FxsHandler; +import org.openhab.binding.freeboxos.internal.handler.HostHandler; +import org.openhab.binding.freeboxos.internal.handler.KeyfobHandler; +import org.openhab.binding.freeboxos.internal.handler.PlayerHandler; +import org.openhab.binding.freeboxos.internal.handler.RepeaterHandler; +import org.openhab.binding.freeboxos.internal.handler.RevolutionHandler; +import org.openhab.binding.freeboxos.internal.handler.ServerHandler; +import org.openhab.binding.freeboxos.internal.handler.ShutterHandler; +import org.openhab.binding.freeboxos.internal.handler.VmHandler; +import org.openhab.binding.freeboxos.internal.handler.WifiStationHandler; +import org.openhab.core.audio.AudioHTTPServer; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.net.HttpServiceUtil; +import org.openhab.core.net.NetworkAddressService; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FreeboxOsHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.freeboxos") +public class FreeboxOsHandlerFactory extends BaseThingHandlerFactory { + private static final String TIMEOUT = "timeout"; + private static final String CALLBACK_URL = "callBackUrl"; + + private final Logger logger = LoggerFactory.getLogger(FreeboxOsHandlerFactory.class); + + private final NetworkAddressService networkAddressService; + private final AudioHTTPServer audioHTTPServer; + private final HttpClient httpClient; + private final ApiHandler apiHandler; + private String callbackURL = ""; + + @Activate + public FreeboxOsHandlerFactory(final @Reference AudioHTTPServer audioHTTPServer, + final @Reference NetworkAddressService networkAddressService, + final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider, + ComponentContext componentContext, Map config) { + super.activate(componentContext); + + this.audioHTTPServer = audioHTTPServer; + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.networkAddressService = networkAddressService; + this.apiHandler = new ApiHandler(httpClient, timeZoneProvider); + + configChanged(config); + } + + @Modified + public void configChanged(Map config) { + String timeout = (String) config.getOrDefault(TIMEOUT, "8"); + apiHandler.setTimeout(TimeUnit.SECONDS.toMillis(Long.parseLong(timeout))); + + callbackURL = (String) config.getOrDefault(CALLBACK_URL, ""); + int port = HttpServiceUtil.getHttpServicePort(bundleContext); + if (callbackURL.isEmpty() && port != -1) { + String openHabIp = Objects.requireNonNull(networkAddressService.getPrimaryIpv4HostAddress()); + // we do not use SSL as it can cause certificate validation issues. + callbackURL = "http://%s:%d".formatted(openHabIp, port); + } + if (callbackURL.isEmpty()) { + logger.warn("Unable to build a correct call back URL to stream media contents"); + return; + } + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (BRIDGE_TYPE_API.equals(thingTypeUID)) { + return new FreeboxOsHandler((Bridge) thing, new FreeboxOsSession(apiHandler), callbackURL, bundleContext, + audioHTTPServer); + } else if (THING_TYPE_FREEPLUG.equals(thingTypeUID)) { + return new FreeplugHandler(thing); + } else if (THING_TYPE_FXS.equals(thingTypeUID)) { + return new FxsHandler(thing); + } else if (THING_TYPE_DECT.equals(thingTypeUID)) { + return new DectHandler(thing); + } else if (THING_TYPE_CALL.equals(thingTypeUID)) { + return new CallHandler(thing); + } else if (THING_TYPE_REVOLUTION.equals(thingTypeUID)) { + return new RevolutionHandler(thing); + } else if (THING_TYPE_DELTA.equals(thingTypeUID)) { + return new ServerHandler(thing); + } else if (THING_TYPE_HOST.equals(thingTypeUID)) { + return new HostHandler(thing); + } else if (THING_TYPE_WIFI_HOST.equals(thingTypeUID)) { + return new WifiStationHandler(thing); + } else if (THING_TYPE_REPEATER.equals(thingTypeUID)) { + return new RepeaterHandler(thing); + } else if (THING_TYPE_VM.equals(thingTypeUID)) { + return new VmHandler(thing); + } else if (THING_TYPE_ACTIVE_PLAYER.equals(thingTypeUID)) { + return new ActivePlayerHandler(thing); + } else if (THING_TYPE_PLAYER.equals(thingTypeUID)) { + return new PlayerHandler(thing); + } else if (Category.BASIC_SHUTTER.getThingTypeUID().equals(thingTypeUID)) { + return new BasicShutterHandler(thing); + } else if (Category.SHUTTER.getThingTypeUID().equals(thingTypeUID)) { + return new ShutterHandler(thing); + } else if (Category.ALARM.getThingTypeUID().equals(thingTypeUID)) { + return new AlarmHandler(thing); + } else if (Category.KFB.getThingTypeUID().equals(thingTypeUID)) { + return new KeyfobHandler(thing); + } else if (Category.CAMERA.getThingTypeUID().equals(thingTypeUID)) { + return new CameraHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ActivePlayerActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ActivePlayerActions.java new file mode 100644 index 00000000000..1023aff9985 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ActivePlayerActions.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.handler.ActivePlayerHandler; +import org.openhab.binding.freeboxos.internal.handler.PlayerHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {ActivePlayerActions} class is responsible to call corresponding actions on Freebox Player with API + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class ActivePlayerActions extends PlayerActions { + private final Logger logger = LoggerFactory.getLogger(ActivePlayerActions.class); + + @RuleAction(label = "reboot freebox player", description = "Reboots the Freebox Player") + public void reboot() { + logger.debug("Player reboot called"); + PlayerHandler localHandler = this.handler; + if (localHandler instanceof ActivePlayerHandler apHandler) { + apHandler.reboot(); + } else { + logger.warn("Freebox Player Action service ThingHandler is null"); + } + } + + public static void reboot(ThingActions actions) { + ((ActivePlayerActions) actions).reboot(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/CallActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/CallActions.java new file mode 100644 index 00000000000..0f8d9b432a1 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/CallActions.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.CallHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {FreeplugActions} class is responsible to call corresponding actions on Freeplugs + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class CallActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(CallActions.class); + private @Nullable CallHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof CallHandler callHandler) { + this.handler = callHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + @RuleAction(label = "clear call queue", description = "Delete all call logged in the queue") + public void reset() { + logger.debug("Call log clear called"); + CallHandler localHandler = handler; + if (localHandler != null) { + localHandler.emptyQueue(); + } else { + logger.warn("Call Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/FreeplugActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/FreeplugActions.java new file mode 100644 index 00000000000..b24af4caea0 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/FreeplugActions.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.FreeplugHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {FreeplugActions} class is responsible to call corresponding actions on Freeplugs + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class FreeplugActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(FreeplugActions.class); + private @Nullable FreeplugHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof FreeplugHandler plugHandler) { + this.handler = plugHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.handler; + } + + @RuleAction(label = "reset freeplug", description = "Resets the Freeplug") + public void reset() { + logger.debug("Freeplug reset requested"); + FreeplugHandler plugHandler = this.handler; + if (plugHandler != null) { + plugHandler.reset(); + } else { + logger.warn("Freeplug Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/HostActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/HostActions.java new file mode 100644 index 00000000000..b076063d0cf --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/HostActions.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.HostHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {HostActions} class is responsible to call corresponding actions on a given lan host + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class HostActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(HostActions.class); + private @Nullable HostHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof HostHandler hostHandler) { + this.handler = hostHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.handler; + } + + @RuleAction(label = "wol host", description = "Awakes a lan host") + public void wol() { + logger.debug("Host WOL called"); + HostHandler hostHandler = this.handler; + if (hostHandler != null) { + hostHandler.wol(); + } else { + logger.warn("LanHost Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/PlayerActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/PlayerActions.java new file mode 100644 index 00000000000..b1e7d619102 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/PlayerActions.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.PlayerHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {PlayerActions} class is responsible to call corresponding actions on Freebox Player + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class PlayerActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(PlayerActions.class); + protected @Nullable PlayerHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof PlayerHandler playerHandler) { + this.handler = playerHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.handler; + } + + @RuleAction(label = "send a key to player", description = "Sends a given key to the player") + public void sendKey(@ActionInput(name = "key") String key) { + logger.debug("Sending key {} to player", key); + PlayerHandler playerHandler = this.handler; + if (playerHandler != null) { + playerHandler.sendKey(key, false, 1); + } else { + logger.warn("Freebox Player Action service ThingHandler is null"); + } + } + + @RuleAction(label = "send a long key to player", description = "Sends a given key to the player and keep it pressed") + public void sendLongKey(@ActionInput(name = "key") String key) { + logger.debug("Sending long press key {} to player", key); + PlayerHandler playerHandler = this.handler; + if (playerHandler != null) { + playerHandler.sendKey(key, true, 1); + } else { + logger.warn("Freebox Player Action service ThingHandler is null"); + } + } + + @RuleAction(label = "send multiple keys to player", description = "Sends multiple keys to the player, comma separated") + public void sendMultipleKeys(@ActionInput(name = "keys") String keys) { + logger.debug("Sending keys {} to player", keys); + PlayerHandler playerHandler = this.handler; + if (playerHandler != null) { + playerHandler.sendMultipleKeys(keys); + } else { + logger.warn("Freebox Player Action service ThingHandler is null"); + } + } + + @RuleAction(label = "send repeating key to player", description = "Sends a given key multiple times to the player") + public void sendKeyRepeat(@ActionInput(name = "key") String key, @ActionInput(name = "count") int count) { + logger.debug("Sending key {} to player {} times", key, count); + PlayerHandler playerHandler = this.handler; + if (playerHandler != null) { + playerHandler.sendKey(key, false, count); + } else { + logger.warn("Freebox Player Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/RepeaterActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/RepeaterActions.java new file mode 100644 index 00000000000..ac33373fa07 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/RepeaterActions.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.RepeaterHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {RepeaterActions} class is responsible to call corresponding actions on Freebox Repeater + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class RepeaterActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(RepeaterActions.class); + private @Nullable RepeaterHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof RepeaterHandler repeaterHandler) { + this.handler = repeaterHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return handler; + } + + @RuleAction(label = "reboot free repeater", description = "Reboots the Free Repeater") + public void reboot() { + logger.debug("Repeater reboot called"); + RepeaterHandler localHandler = this.handler; + if (localHandler != null) { + localHandler.reboot(); + } else { + logger.warn("Repeater Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ServerActions.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ServerActions.java new file mode 100644 index 00000000000..4c5aea8ade9 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/action/ServerActions.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.action; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.handler.ServerHandler; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {ServerActions} class is responsible to call corresponding actions on Freebox Server + * + * @author Gaël L'hopital - Initial contribution + */ +@ThingActionsScope(name = "freeboxos") +@NonNullByDefault +public class ServerActions implements ThingActions { + private final Logger logger = LoggerFactory.getLogger(ServerActions.class); + private @Nullable ServerHandler handler; + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof ServerHandler serverHandler) { + this.handler = serverHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.handler; + } + + @RuleAction(label = "reboot freebox server", description = "Reboots the Freebox Server") + public void reboot() { + logger.debug("Server reboot called"); + ServerHandler serverHandler = this.handler; + if (serverHandler != null) { + serverHandler.reboot(); + } else { + logger.warn("Freebox Action service ThingHandler is null"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/ApiHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/ApiHandler.java new file mode 100644 index 00000000000..df5509cf687 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/ApiHandler.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpStatus.Code; +import org.openhab.binding.freeboxos.internal.api.deserialization.ForegroundAppDeserializer; +import org.openhab.binding.freeboxos.internal.api.deserialization.ListDeserializer; +import org.openhab.binding.freeboxos.internal.api.deserialization.StrictEnumTypeAdapterFactory; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.ForegroundApp; +import org.openhab.core.i18n.TimeZoneProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; + +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; +import inet.ipaddr.MACAddressString; +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link ApiHandler} is responsible for sending requests toward a given url and transform the answer in appropriate + * DTO. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ApiHandler { + public static final String AUTH_HEADER = "X-Fbx-App-Auth"; + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private static final String CONTENT_TYPE = "application/json; charset=" + DEFAULT_CHARSET.name(); + + private final Logger logger = LoggerFactory.getLogger(ApiHandler.class); + private final HttpClient httpClient; + private final Gson gson; + + private long timeoutInMs = TimeUnit.SECONDS.toMillis(8); + + public ApiHandler(HttpClient httpClient, TimeZoneProvider timeZoneProvider) { + this.httpClient = httpClient; + this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(ZonedDateTime.class, + (JsonDeserializer) (json, type, jsonDeserializationContext) -> { + long timestamp = json.getAsJsonPrimitive().getAsLong(); + Instant i = Instant.ofEpochSecond(timestamp); + return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone()); + }) + .registerTypeAdapter(MACAddress.class, + (JsonDeserializer) (json, type, + jsonDeserializationContext) -> new MACAddressString(json.getAsString()).getAddress()) + .registerTypeAdapter(IPAddress.class, + (JsonDeserializer) (json, type, + jsonDeserializationContext) -> new IPAddressString(json.getAsString()).getAddress()) + .registerTypeAdapter(ForegroundApp.class, new ForegroundAppDeserializer()) + .registerTypeAdapter(List.class, new ListDeserializer()).serializeNulls() + .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory()).create(); + } + + public synchronized T executeUri(URI uri, HttpMethod method, Class clazz, @Nullable String sessionToken, + @Nullable Object payload) throws FreeboxException, InterruptedException { + logger.debug("executeUrl {}: {} ", method, uri); + + Request request = httpClient.newRequest(uri).method(method).timeout(timeoutInMs, TimeUnit.MILLISECONDS) + .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE); + + if (sessionToken != null) { + request.header(AUTH_HEADER, sessionToken); + } + + if (payload != null) { + request.content(new StringContentProvider(serialize(payload), DEFAULT_CHARSET), null); + } + + try { + ContentResponse response = request.send(); + + Code statusCode = HttpStatus.getCode(response.getStatus()); + + if (statusCode != Code.OK && statusCode != Code.FORBIDDEN) { + throw new FreeboxException(statusCode.getMessage()); + } + + String content = new String(response.getContent(), DEFAULT_CHARSET); + T result = deserialize(clazz, content); + logger.trace("executeUrl {} - {} returned {}", method, uri, content); + + if (statusCode == Code.OK) { + return result; + } else if (statusCode == Code.FORBIDDEN) { + logger.debug("Fobidden, serviceReponse was {}, ", content); + if (result instanceof Response errorResponse) { + throw new FreeboxException(errorResponse.getErrorCode(), errorResponse.getMsg()); + } + } + + throw new FreeboxException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString()); + } catch (TimeoutException | ExecutionException e) { + throw new FreeboxException(e, "Exception while calling %s", request.getURI()); + } + } + + public T deserialize(Class clazz, String json) { + @Nullable + T result = gson.fromJson(json, clazz); + if (result != null) { + return result; + } + throw new IllegalArgumentException("Null result deserializing '%s', please file a bug report.".formatted(json)); + } + + public String serialize(Object payload) { + return gson.toJson(payload); + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public void setTimeout(long millis) { + timeoutInMs = millis; + logger.debug("Timeout set to {} ms", timeoutInMs); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxException.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxException.java new file mode 100644 index 00000000000..5b04f63a160 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxException.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.Response.ErrorCode; + +/** + * Exception for errors when using the Freebox API + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxException extends Exception { + private static final long serialVersionUID = 9197365222439228186L; + private ErrorCode errorCode = ErrorCode.NONE; + + public FreeboxException(String format, Object... args) { + super(String.format(format, args)); + } + + public FreeboxException(Exception cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public FreeboxException(ErrorCode errorCode, String message) { + this(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxOsIconProvider.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxOsIconProvider.java new file mode 100644 index 00000000000..2895adde5b4 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxOsIconProvider.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpStatus.Code; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.ui.icon.AbstractResourceIconProvider; +import org.openhab.core.ui.icon.IconProvider; +import org.openhab.core.ui.icon.IconSet; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@FreeboxOsIconProvider} delivers icons provided by FreeboxOS + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = { IconProvider.class }) +public class FreeboxOsIconProvider extends AbstractResourceIconProvider { + + private final Logger logger = LoggerFactory.getLogger(FreeboxOsIconProvider.class); + + private final HttpClient httpClient; + private final UriBuilder uriBuilder; + + @Activate + public FreeboxOsIconProvider(final @Reference TranslationProvider i18nProvider, + final @Reference HttpClientFactory httpClientFactory) { + super(i18nProvider); + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.uriBuilder = UriBuilder.fromPath("/").scheme("http").host(FreeboxTlsCertificateProvider.DEFAULT_NAME) + .path("resources/images/home/pictos"); + } + + @Override + public Set getIconSets(@Nullable Locale locale) { + return Set.of(); + } + + @Override + protected Integer getPriority() { + return 4; + } + + @Override + protected @Nullable InputStream getResource(String iconSetId, String resourceName) { + URI uri = uriBuilder.clone().path(resourceName).build(); + Request request = httpClient.newRequest(uri).method(HttpMethod.GET); + + try { + ContentResponse response = request.send(); + if (HttpStatus.getCode(response.getStatus()) == Code.OK) { + return new ByteArrayInputStream(response.getContent()); + } + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.warn("Error getting icon {}: {}", resourceName, e.getMessage()); + } + return null; + } + + @Override + protected boolean hasResource(String iconSetId, String resourceName) { + return resourceName.contains(".png") && getResource(iconSetId, resourceName) != null; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxTlsCertificateProvider.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxTlsCertificateProvider.java new file mode 100644 index 00000000000..47111248e99 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/FreeboxTlsCertificateProvider.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import java.net.URL; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.net.http.TlsCertificateProvider; +import org.osgi.service.component.annotations.Component; + +/** + * Provides a CertificateManager for the Freebox SSL certificate + * + * @author Gaël L'hopital - Initial Contribution + */ +@Component +@NonNullByDefault +public class FreeboxTlsCertificateProvider implements TlsCertificateProvider { + + private static final String CERTIFICATE_NAME = "freeboxECCRootCA.crt"; + + public static final String DEFAULT_NAME = "mafreebox.freebox.fr"; + + @Override + public String getHostName() { + return DEFAULT_NAME; + } + + @Override + public URL getCertificate() { + URL resource = Thread.currentThread().getContextClassLoader().getResource(CERTIFICATE_NAME); + if (resource != null) { + return resource; + } + throw new IllegalStateException("Certificate '%s' not found or not accessible".formatted(CERTIFICATE_NAME)); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/IliadboxTlsCertificateProvider.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/IliadboxTlsCertificateProvider.java new file mode 100644 index 00000000000..c6e25496588 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/IliadboxTlsCertificateProvider.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import java.net.URL; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.net.http.TlsCertificateProvider; +import org.osgi.service.component.annotations.Component; + +/** + * Provides a CertificateManager for the IliadBox SSL certificate + * + * @author Gaël L'hopital - Initial Contribution + */ +@Component +@NonNullByDefault +public class IliadboxTlsCertificateProvider implements TlsCertificateProvider { + + private static final String CERTIFICATE_NAME = "iliadboxECCRootCA.crt"; + + public static final String DEFAULT_NAME = "myiliadbox.iliad.it"; + + @Override + public String getHostName() { + return DEFAULT_NAME; + } + + @Override + public URL getCertificate() { + URL resource = Thread.currentThread().getContextClassLoader().getResource(CERTIFICATE_NAME); + if (resource != null) { + return resource; + } + throw new IllegalStateException("Certificate '%s' not found or not accessible".formatted(CERTIFICATE_NAME)); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/PermissionException.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/PermissionException.java new file mode 100644 index 00000000000..d18c0c52e60 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/PermissionException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.LoginManager; + +/** + * Exception for errors when Session require missing permission + * + * @author ben12 - Initial contribution + */ +@NonNullByDefault +public class PermissionException extends FreeboxException { + private static final long serialVersionUID = 3965810786699311126L; + + private final LoginManager.Permission permission; + + public PermissionException(LoginManager.Permission permission, String format, Object... args) { + super(format, args); + this.permission = permission; + } + + public LoginManager.Permission getPermission() { + return permission; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/Response.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/Response.java new file mode 100644 index 00000000000..1b95cf408c9 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/Response.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.LoginManager; + +/** + * Defines an API result that returns a single object + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class Response { + public static enum ErrorCode { + AUTH_REQUIRED, + BAD_LOGIN, + TOO_SHORT, + IN_DICTIONNARY, + BAD_XKCD, + NOT_ENOUGH_DIFFERENT_CHARS, + INVALID_TOKEN, + PENDING_TOKEN, + INSUFFICIENT_RIGHTS, + DENIED_FROM_EXTERNAL_IP, + INVALID_REQUEST, + RATELIMITED, + NEW_APPS_DENIED, + APPS_AUTHORIZATION_DENIED, + APPS_AUTHORIZATION_TIMEOUT, + PASSWORD_RESET_DENIED, + APPS_DENIED, + INTERNAL_ERROR, + SERVICE_DOWN, + DISK_FULL, + OP_FAILED, + DISK_BUSY, + ARRAY_START_FAILED, + ARRAY_STOP_FAILED, + ARRAY_NOT_FOUND, + INVAL, + NODEV, + NOENT, + NETDOWN, + BUSY, + INVALID_PORT, + INSECURE_PASSWORD, + INVALID_PROVIDER, + INVALID_NEXT_HOP, + INVALID_API_VERSION, + INVAL_WPS_MACFILTER, + INVAL_WPS_NEEDS_CCMP, + INVALID_ID, + PATH_NOT_FOUND, + ACCESS_DENIED, + DESTINATION_CONFLICT, + CANCELLED, + TASK_NOT_FOUND, + HTTP, + INVALID_URL, + INVALID_OPERATION, + INVALID_FILE, + CTX_FILE_ERROR, + HIBERNATING, + TOO_MANY_TASKS, + EXISTS, + EXIST, + CONNECTION_REFUSED, + NO_FREEBOX, + ALREADY_AUTHORIZED, + ECRC, + ERR_001, + ERR_002, + ERR_003, + ERR_004, + ERR_005, + ERR_009, + ERR_010, + ERR_030, + ERR_031, + NONE, + UNKNOWN; + } + + private ErrorCode errorCode = ErrorCode.NONE; + private LoginManager.Permission missingRight = LoginManager.Permission.NONE; + private String msg = ""; + private List result = List.of(); + private boolean success; + + // In some cases I did not understand deserialization can still produce null result + @SuppressWarnings("null") + public List getResult() { + List localResult = result; + return localResult != null ? localResult : List.of(); + } + + public boolean isSuccess() { + return success; + } + + public LoginManager.Permission getMissingRight() { + return missingRight; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + public String getMsg() { + return msg; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ForegroundAppDeserializer.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ForegroundAppDeserializer.java new file mode 100644 index 00000000000..74c8305cd65 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ForegroundAppDeserializer.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.deserialization; + +import java.lang.reflect.Type; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.ForegroundApp; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.PlayerContext; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.TvContext; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * Custom deserializer to handle {@link ForegroundApp} object + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ForegroundAppDeserializer implements JsonDeserializer { + + @Override + public @NonNull ForegroundApp deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + Object obj; + + String thePackage = json.getAsJsonObject().get("package").getAsString(); + JsonElement jsonElement2 = json.getAsJsonObject().get("context"); + if (jsonElement2 == null) { + obj = null; + } else if ("fr.freebox.tv".equals(thePackage)) { + obj = context.deserialize(jsonElement2, TvContext.class); + } else { + obj = context.deserialize(jsonElement2, PlayerContext.class); + } + + int packageId = json.getAsJsonObject().get("package_id").getAsInt(); + String curlUrl = json.getAsJsonObject().get("cur_url").getAsString(); + Objects.requireNonNull(thePackage); + return new ForegroundApp(packageId, curlUrl, obj, thePackage); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ListDeserializer.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ListDeserializer.java new file mode 100644 index 00000000000..0e403d495a5 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/ListDeserializer.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.deserialization; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * The {@link ListDeserializer} is a specialized deserializer aimed to transform a null object, a single object or + * a list of objects into a list containing 0, 1 or n elements. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ListDeserializer implements JsonDeserializer> { + + @Override + public @NonNull List deserialize(@Nullable JsonElement json, @Nullable Type clazz, + @Nullable JsonDeserializationContext context) throws JsonParseException { + if (json != null && clazz != null && context != null) { + JsonArray jsonArray = toJsonArray(json); + ArrayList result = new ArrayList<>(jsonArray != null ? jsonArray.size() : 0); + + if (jsonArray != null) { + Type[] typeArguments = ((ParameterizedType) clazz).getActualTypeArguments(); + if (typeArguments.length > 0) { + Type objectType = typeArguments[0]; + for (int i = 0; i < jsonArray.size(); i++) { + result.add(context.deserialize(jsonArray.get(i), objectType)); + } + return result; + } + } + } + return List.of(); + } + + private @Nullable JsonArray toJsonArray(JsonElement json) { + if (json instanceof JsonArray) { + return json.getAsJsonArray(); + } else if (json instanceof JsonObject) { + JsonArray jsonArray = new JsonArray(); + if (json.getAsJsonObject().size() > 0) { + jsonArray.add(json); + } + return jsonArray; + } + return null; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/StrictEnumTypeAdapterFactory.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/StrictEnumTypeAdapterFactory.java new file mode 100644 index 00000000000..3c881fcb5e6 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/deserialization/StrictEnumTypeAdapterFactory.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.deserialization; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Enforces a fallback to UNKNOWN when deserializing enum types, marked as @NonNull whereas they were valued + * to null if the appropriate value is absent. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class StrictEnumTypeAdapterFactory implements TypeAdapterFactory { + private static final StringReader UNKNOWN = new StringReader("\"UNKNOWN\""); + + @Override + public @Nullable TypeAdapter create(@NonNullByDefault({}) Gson gson, + @NonNullByDefault({}) TypeToken type) { + @SuppressWarnings("unchecked") + Class rawType = (Class) type.getRawType(); + return rawType.isEnum() ? newStrictEnumAdapter(gson.getDelegateAdapter(this, type)) : null; + } + + private TypeAdapter newStrictEnumAdapter(TypeAdapter delegateAdapter) { + return new TypeAdapter() { + @Override + public void write(JsonWriter out, @Nullable T value) throws IOException { + delegateAdapter.write(out, value); + } + + @Override + public @NonNull T read(JsonReader in) throws IOException { + String searched = in.nextString().toUpperCase().replace("/", "_").replace("-", "_"); + JsonReader delegateReader = new JsonReader(new StringReader('"' + searched + '"')); + @Nullable + T value = delegateAdapter.read(delegateReader); + delegateReader.close(); + if (value == null) { + UNKNOWN.reset(); + value = delegateAdapter.read(new JsonReader(UNKNOWN)); + } + return Objects.requireNonNull(value); + } + }; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/APManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/APManager.java new file mode 100644 index 00000000000..233046da0a8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/APManager.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link APManager} is the Java class used to handle api requests related to wifi access points + * provided by the Freebox Server + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class APManager extends ListableRest { + private static final String PATH = "ap"; + private static final String STATIONS_PATH = "stations"; + + protected static record WifiInformation(String ssid, String band, int signal) { // Valid RSSI goes from -120 to 0 + } + + public static record LanAccessPoint(String mac, String type, String uid, @Nullable String connectivityType, + long rxBytes, // received bytes (from station to Freebox) + long txBytes, // transmitted bytes (from Freebox to station) + long txRate, // reception data rate (in bytes/s) + long rxRate, // transmission data rate (in bytes/s) + WifiInformation wifiInformation) { + + public int getSignal() { + return wifiInformation.signal(); + } + + public @Nullable String getSsid() { + return wifiInformation().ssid(); + } + } + + private static enum State { + ASSOCIATED, + AUTHENTICATED, + UNKNOWN; + } + + public static record Station(String id, MACAddress mac, String bssid, @Nullable String hostname, LanHost host, + State state, int inactive, int connDuration, // + long rxBytes, // received bytes (from station to Freebox) + long txBytes, // transmitted bytes (from Freebox to station) + long txRate, // reception data rate (in bytes/s) + long rxRate, // transmission data rate (in bytes/s) + int signal) { // signal attenuation (in dB) + + public @Nullable String getSsid() { + LanAccessPoint accessPoint = host.accessPoint(); + return accessPoint != null ? accessPoint.getSsid() : null; + } + + public @Nullable ZonedDateTime getLastSeen() { + return host.getLastSeen(); + } + } + + protected static record ApStatus(ApState state, int channelWidth, int primaryChannel, int secondaryChannel, + int dfsCacRemainingTime, boolean dfsDisabled) { + private static enum ApState { + SCANNING, // Ap is probing wifi channels + NO_PARAM, // Ap is not configured + BAD_PARAM, // Ap has an invalid configuration + DISABLED, // Ap is permanently disabled + DISABLED_PLANNING, // Ap is currently disabled according to planning + NO_ACTIVE_BSS, // Ap has no active BSS + STARTING, // Ap is starting + ACS, // Ap is selecting the best available channel + HT_SCAN, // Ap is scanning for other access point + DFS, // Ap is performing dynamic frequency selection + ACTIVE, // Ap is active + FAILED, // Ap has failed to start + UNKNOWN; + } + } + + protected static record WifiAp(int id, String name, ApStatus status) { + } + + private class ApHostsResponse extends Response { + } + + protected class APResponse extends Response { + } + + public APManager(FreeboxOsSession session, UriBuilder uriBuilder) throws FreeboxException { + super(session, LoginManager.Permission.NONE, APResponse.class, uriBuilder.path(PATH)); + } + + private List getApStations(int apId) throws FreeboxException { + return get(ApHostsResponse.class, Integer.toString(apId), STATIONS_PATH); + } + + public List getStations() throws FreeboxException { + List hosts = new ArrayList<>(); + for (WifiAp ap : getDevices()) { + hosts.addAll(getApStations(ap.id)); + } + return hosts; + } + + public Optional getStation(MACAddress mac) throws FreeboxException { + return getStations().stream().filter(host -> host.mac().equals(mac)).findFirst(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AfpManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AfpManager.java new file mode 100644 index 00000000000..ed077efada4 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AfpManager.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link AfpManager} is the Java class used to handle api requests related to Afp shares + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AfpManager extends ConfigurableRest { + private static final String AFP_PATH = "afp"; + + protected static class ConfigResponse extends Response { + } + + protected static record Afp(boolean enabled, boolean guestAllow, ServerType serverType, @Nullable String loginName, + @Nullable String loginPassword) { + private static enum ServerType { + POWERBOOK, + POWERMAC, + MACMINI, + IMAC, + MACBOOK, + MACBOOKPRO, + MACBOOKAIR, + MACPRO, + APPLETV, + AIRPORT, + XSERVE, + UNKNOWN; + } + } + + public AfpManager(FreeboxOsSession session, UriBuilder uriBuilder) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, uriBuilder.path(AFP_PATH), null); + } + + public boolean getStatus() throws FreeboxException { + return getConfig().enabled; + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + Afp config = getConfig(); + Afp newConfig = new Afp(enabled, config.guestAllow, config.serverType, config.loginName, config.loginPassword); + return setConfig(newConfig).enabled; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AirMediaManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AirMediaManager.java new file mode 100644 index 00000000000..8b8ddc25852 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/AirMediaManager.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link AirMediaManager} is the Java class used to handle api requests related to air media global configuration + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AirMediaManager extends ConfigurableRest { + private static final String PATH = "airmedia"; + + protected static record Config(boolean enabled) { + } + + protected static class ConfigResponse extends Response { + } + + public AirMediaManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + session.addManager(MediaReceiverManager.class, new MediaReceiverManager(session, getUriBuilder())); + } + + public boolean getStatus() throws FreeboxException { + return getConfig().enabled(); + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + return setConfig(new Config(enabled)).enabled(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/CallManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/CallManager.java new file mode 100644 index 00000000000..54444ba48fa --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/CallManager.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.THING_CALL; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link CallManager} is the Java class used to handle api requests related to calls + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class CallManager extends RestManager { + private static final String LOG_SUB_PATH = "log/"; + private static final String DELETE_ACTION = "delete_all"; + + private static class Calls extends Response { + } + + public static enum Type { + ACCEPTED, + MISSED, + OUTGOING, + INCOMING, + UNKNOWN; + } + + public static record Call(Type type, // + ZonedDateTime datetime, // Call creation timestamp. + String number, // Calling or called number + int duration, // Call duration in seconds. + String name) { + + public @Nullable String name() { + return name.equals(number) ? null : name; + } + } + + public CallManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.CALLS, session.getUriBuilder().path(THING_CALL)); + } + + // Retrieves a sorted list of all call entries + public List getCallEntries() throws FreeboxException { + List callList = new ArrayList<>( + get(Calls.class, LOG_SUB_PATH).stream().sorted(Comparator.comparing(Call::datetime)).toList()); + Call last = callList.get(callList.size() - 1); + // The INCOMING type call can only be set on the last call if its duration is 0; + if (last.type == Type.MISSED && last.duration == 0) { + callList.remove(callList.size() - 1); + callList.add(new Call(Type.INCOMING, last.datetime, last.number, 0, last.name)); + } + return callList; + } + + public void emptyQueue() throws FreeboxException { + post(LOG_SUB_PATH, DELETE_ACTION); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConfigurableRest.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConfigurableRest.java new file mode 100644 index 00000000000..7d57f93a394 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConfigurableRest.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link ConfigurableRest} is the Java class used to handle portions of the Api that accept to get and set + * configuration based on a given DTO + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ConfigurableRest> extends RestManager { + protected static final String CONFIG_PATH = "config"; + + private final Class responseClazz; + private final @Nullable String configPath; + + protected ConfigurableRest(FreeboxOsSession session, LoginManager.Permission required, Class responseClazz, + UriBuilder uri, @Nullable String configPath) throws FreeboxException { + super(session, required, uri); + this.responseClazz = responseClazz; + this.configPath = configPath; + } + + public T getConfig() throws FreeboxException { + return configPath != null ? getSingle(responseClazz, configPath) : getSingle(responseClazz); + } + + protected T setConfig(T config) throws FreeboxException { + return configPath != null ? put(responseClazz, config, configPath) : put(responseClazz, config); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConnectionManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConnectionManager.java new file mode 100644 index 00000000000..c500847a641 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ConnectionManager.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +import inet.ipaddr.IPAddress; + +/** + * The {@link ConnectionManager} is the Java class used to handle api requests related to connection + * + * https://dev.freebox.fr/sdk/os/system/# + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ConnectionManager extends ConfigurableRest { + private static final String PATH = "connection"; + + protected static class StatusResponse extends Response { + } + + private static enum State { + GOING_UP, + UP, + GOING_DOWN, + DOWN, + UNKNOWN; + } + + private static enum Type { + ETHERNET, + RFC2684, + PPPOATM, + UNKNOWN; + } + + private static enum Media { + FTTH, + ETHERNET, + XDSL, + BACKUP_4G, + UNKNOWN; + } + + public static record Status(State state, Type type, Media media, @Nullable List ipv4PortRange, + @Nullable IPAddress ipv4, // This can be null if state is not up + @Nullable IPAddress ipv6, // This can be null if state is not up + long rateUp, // current upload rate in byte/s + long rateDown, // current download rate in byte/s + long bandwidthUp, // available upload bandwidth in bit/s + long bandwidthDown, // available download bandwidth in bit/s + long bytesUp, // total uploaded bytes since last connection + long bytesDown // total downloaded bytes since last connection + ) { + } + + public ConnectionManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, StatusResponse.class, session.getUriBuilder().path(PATH), null); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java new file mode 100644 index 00000000000..d67b3edc7e9 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeboxOsSession.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.freeboxos.internal.api.ApiHandler; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.PermissionException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.Response.ErrorCode; +import org.openhab.binding.freeboxos.internal.api.rest.LoginManager.Session; +import org.openhab.binding.freeboxos.internal.config.FreeboxOsConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FreeboxOsSession} is responsible for sending requests toward a given url and transform the answer in + * appropriate dto. + * + * @author Gaël L'Hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxOsSession { + private static final String API_VERSION_PATH = "api_version"; + + private final Logger logger = LoggerFactory.getLogger(FreeboxOsSession.class); + private final Map, RestManager> restManagers = new HashMap<>(); + private final ApiHandler apiHandler; + + private @NonNullByDefault({}) UriBuilder uriBuilder; + private @Nullable Session session; + private String appToken = ""; + + public static enum BoxModel { + FBXGW_R1_FULL, // Freebox Server (v6) revision 1 + FBXGW_R2_FULL, // Freebox Server (v6) revision 2 + FBXGW_R1_MINI, // Freebox Mini revision 1 + FBXGW_R2_MINI, // Freebox Mini revision 2 + FBXGW_R1_ONE, // Freebox One revision 1 + FBXGW_R2_ONE, // Freebox One revision 2 + FBXGW7_R1_FULL, // Freebox v7 revision 1 + UNKNOWN; + } + + public static record ApiVersion(String apiBaseUrl, @Nullable String apiDomain, String apiVersion, BoxModel boxModel, + @Nullable String boxModelName, String deviceName, String deviceType, boolean httpsAvailable, int httpsPort, + String uid) { + + /** + * @return a string like eg: '/api/v8' + */ + private String baseUrl() { + return "%sv%s".formatted(apiBaseUrl, apiVersion.split("\\.")[0]); + } + } + + public FreeboxOsSession(ApiHandler apiHandler) { + this.apiHandler = apiHandler; + } + + public void initialize(FreeboxOsConfiguration config) throws FreeboxException, InterruptedException { + ApiVersion version = apiHandler.executeUri(config.getUriBuilder(API_VERSION_PATH).build(), HttpMethod.GET, + ApiVersion.class, null, null); + this.uriBuilder = config.getUriBuilder(version.baseUrl()); + getManager(LoginManager.class); + getManager(NetShareManager.class); + getManager(LanManager.class); + getManager(WifiManager.class); + getManager(FreeplugManager.class); + getManager(AirMediaManager.class); + } + + public void openSession(String appToken) throws FreeboxException { + Session newSession = getManager(LoginManager.class).openSession(appToken); + getManager(WebSocketManager.class).openSession(newSession.sessionToken()); + session = newSession; + this.appToken = appToken; + } + + public String grant() throws FreeboxException { + return getManager(LoginManager.class).checkGrantStatus(); + } + + public void closeSession() { + Session currentSession = session; + if (currentSession != null) { + try { + getManager(WebSocketManager.class).closeSession(); + getManager(LoginManager.class).closeSession(); + session = null; + } catch (FreeboxException e) { + logger.warn("Error closing session: {}", e.getMessage()); + } + } + appToken = ""; + restManagers.clear(); + } + + private synchronized > List execute(URI uri, HttpMethod method, Class clazz, + boolean retryAuth, int retryCount, @Nullable Object aPayload) throws FreeboxException { + try { + T response = apiHandler.executeUri(uri, method, clazz, getSessionToken(), aPayload); + if (response.getErrorCode() == ErrorCode.INTERNAL_ERROR && retryCount > 0) { + return execute(uri, method, clazz, false, retryCount - 1, aPayload); + } else if (retryAuth && response.getErrorCode() == ErrorCode.AUTH_REQUIRED) { + openSession(appToken); + return execute(uri, method, clazz, false, retryCount, aPayload); + } + if (!response.isSuccess()) { + throw new FreeboxException("Api request failed: %s", response.getMsg()); + } + return response.getResult(); + } catch (FreeboxException e) { + if (ErrorCode.AUTH_REQUIRED.equals(e.getErrorCode())) { + openSession(appToken); + return execute(uri, method, clazz, false, retryCount, aPayload); + } + throw e; + } catch (InterruptedException ignored) { + return List.of(); + } + } + + public > List execute(URI uri, HttpMethod method, Class clazz, + @Nullable Object aPayload) throws FreeboxException { + return execute(uri, method, clazz, getSessionToken() != null, 3, aPayload); + } + + @SuppressWarnings("unchecked") + public synchronized T getManager(Class clazz) throws FreeboxException { + RestManager manager = restManagers.get(clazz); + if (manager == null) { + try { + Constructor managerConstructor = clazz.getConstructor(FreeboxOsSession.class); + manager = addManager(clazz, managerConstructor.newInstance(this)); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof PermissionException) { + throw (PermissionException) cause; + } + throw new FreeboxException(e, "Unable to call RestManager constructor for %s", clazz.getName()); + } catch (ReflectiveOperationException e) { + throw new FreeboxException(e, "Unable to call RestManager constructor for %s", clazz.getName()); + } + } + return (T) manager; + } + + public T addManager(Class clazz, T manager) { + restManagers.put(clazz, manager); + return manager; + } + + boolean hasPermission(LoginManager.Permission required) { + Session currentSession = session; + return currentSession != null ? currentSession.hasPermission(required) : false; + } + + private @Nullable String getSessionToken() { + Session currentSession = session; + return currentSession != null ? currentSession.sessionToken() : null; + } + + public UriBuilder getUriBuilder() { + return uriBuilder.clone(); + } + + public ApiHandler getApiHandler() { + return apiHandler; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeplugManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeplugManager.java new file mode 100644 index 00000000000..0444f878303 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FreeplugManager.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.THING_FREEPLUG; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link FreeplugManager} is the Java class used to handle api requests related to freeplugs + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeplugManager extends RestManager { + private static final String RESET_ACTION = "reset"; + + private static class Networks extends Response { + } + + public static enum NetRole { + STA, // Freeplug station + PCO, // Freeplug proxy coordinator + CCO, // Central Coordinator + UNKNOWN; + } + + private enum Status { + UP, + DOWN, + UNKNOWN + } + + public static record Freeplug(MACAddress id, String netId, // Id of the network holding the plug + boolean local, // if true the Freeplug is connected directly to the Freebox + NetRole netRole, // Freeplug network role + String model, Status ethPortStatus, // + boolean ethFullDuplex, // ethernet link is full duplex + boolean hasNetwork, // is connected to the network + int ethSpeed, // ethernet port speed + int inactive, // seconds since last activity + int rxRate, // rx rate (from the freeplugs to the “cco” freeplug) (in Mb/s) -1 if not available + int txRate) { // tx rate (from the “cco” freeplug to the freeplugs) (in Mb/s) -1 if not available + } + + private static record Network(MACAddress id, List members) { + } + + public FreeplugManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(THING_FREEPLUG)); + } + + // Most of the users will host only one CPL network on their server, so we hide the network level in the manager + public List getPlugs() throws FreeboxException { + return get(Networks.class).stream().map(Network::members).flatMap(List::stream).toList(); + } + + public Optional getPlug(MACAddress mac) throws FreeboxException { + return getPlugs().stream().filter(plug -> plug.id.equals(mac)).findFirst(); + } + + public void reboot(MACAddress mac) throws FreeboxException { + post(mac.toColonDelimitedString(), RESET_ACTION); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FtpManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FtpManager.java new file mode 100644 index 00000000000..6be5801e895 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/FtpManager.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link FtpManager} is the Java class used to handle api requests related to ftp + * + * https://dev.freebox.fr/sdk/os/system/# + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FtpManager extends ConfigurableRest { + private static final String PATH = "ftp"; + + protected static class ConfigResponse extends Response { + } + + protected static record Config(boolean enabled, boolean allowAnonymous, boolean allowAnonymousWrite, + boolean allowRemoteAccess, boolean weakPassword, int portCtrl, int portData, String remoteDomain) { + } + + public FtpManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + } + + public boolean getStatus() throws FreeboxException { + return getConfig().enabled(); + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + Config oldConfig = getConfig(); + Config newConfig = new Config(enabled, oldConfig.allowAnonymous, oldConfig.allowAnonymousWrite, + oldConfig.allowRemoteAccess, oldConfig.weakPassword, oldConfig.portCtrl, oldConfig.portData, + oldConfig.remoteDomain); + return setConfig(newConfig).enabled(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/HomeManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/HomeManager.java new file mode 100644 index 00000000000..6fd9187016b --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/HomeManager.java @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.BINDING_ID; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.core.thing.ThingTypeUID; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link HomeManager} is the Java class used to handle api requests related to home + * + * @author ben12 - Initial contribution + */ +@NonNullByDefault +public class HomeManager extends RestManager { + private static final String PATH = "home"; + private static final String NODES_PATH = "nodes"; + private static final String ENDPOINTS_PATH = "endpoints"; + + private static class EndpointStateResponse extends Response { + } + + private static class HomeNodesResponse extends Response { + } + + private static enum AccessType { + R, + W, + RW, + UNKNOWN; + } + + private static enum DisplayType { + TEXT, + ICON, + BUTTON, + SLIDER, + TOGGLE, + COLOR, + WARNING, + UNKNOWN; + } + + private static record EndpointValue(T value) { + } + + private static record EndpointUi(AccessType access, DisplayType display, String iconUrl, @Nullable String unit) { + } + + private static enum ValueType { + BOOL, + INT, + FLOAT, + VOID, + STRING, + UNKNOWN; + } + + public static record EndpointState(@Nullable String value, ValueType valueType, long refresh) { + public boolean asBoolean() { + String local = value; + return local != null ? Boolean.valueOf(local) : false; + } + + public int asInt() { + String local = value; + return local != null ? Integer.valueOf(local) : Integer.MIN_VALUE; + } + + public @Nullable String value() { + return value; + } + } + + public static enum EpType { + SIGNAL, + SLOT, + UNKNOWN; + + public String asConfId() { + return name().toLowerCase(); + } + } + + private static record LogEntry(long timestamp, int value) { + } + + public static record Endpoint(int id, String name, String label, EpType epType, Visibility visibility, int refresh, + ValueType valueType, EndpointUi ui, @Nullable String category, Object value, List history) { + private static enum Visibility { + INTERNAL, + NORMAL, + DASHBOARD, + UNKNOWN; + } + } + + private static enum Status { + UNREACHABLE, + DISABLED, + ACTIVE, + UNPAIRED, + UNKNOWN; + } + + public static enum Category { + BASIC_SHUTTER, + SHUTTER, + ALARM, + KFB, + CAMERA, + UNKNOWN; + + private final ThingTypeUID thingTypeUID; + + Category() { + thingTypeUID = new ThingTypeUID(BINDING_ID, name().toLowerCase()); + } + + public ThingTypeUID getThingTypeUID() { + return thingTypeUID; + } + } + + public static record NodeType(@SerializedName("abstract") boolean _abstract, List endpoints, + boolean generic, String icon, String inherit, String label, String name, boolean physical) { + } + + public static record HomeNode(int id, @Nullable String name, @Nullable String label, Category category, + Status status, List showEndpoints, Map props, NodeType type) { + } + + public HomeManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.HOME, session.getUriBuilder().path(PATH)); + } + + public List getHomeNodes() throws FreeboxException { + return get(HomeNodesResponse.class, NODES_PATH); + } + + public HomeNode getHomeNode(int nodeId) throws FreeboxException { + return getSingle(HomeNodesResponse.class, NODES_PATH, Integer.toString(nodeId)); + } + + public @Nullable EndpointState getEndpointsState(int nodeId, int stateSignalId) throws FreeboxException { + return getSingle(EndpointStateResponse.class, ENDPOINTS_PATH, String.valueOf(nodeId), + String.valueOf(stateSignalId)); + } + + public boolean putCommand(int nodeId, int stateSignalId, T value) throws FreeboxException { + put(new EndpointValue(value), ENDPOINTS_PATH, String.valueOf(nodeId), String.valueOf(stateSignalId)); + return true; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanBrowserManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanBrowserManager.java new file mode 100644 index 00000000000..694d964f22d --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanBrowserManager.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.APManager.LanAccessPoint; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.InterfacesResponse; + +import inet.ipaddr.IPAddress; +import inet.ipaddr.IPAddressString; +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link LanBrowserManager} is the Java class used to handle api requests related to lan + * + * https://dev.freebox.fr/sdk/os/system/# + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LanBrowserManager extends ListableRest { + private static final IPAddress NULL_IP = new IPAddressString("0.0.0.0").getAddress(); + private static final String PATH = "browser"; + private static final String INTERFACES = "interfaces"; + private static final String WOL_ACTION = "wol"; + + protected static class HostsResponse extends Response { + } + + protected static class InterfacesResponse extends Response { + } + + public static enum Source { + DHCP, + NETBIOS, + MDNS, + MDNS_SRV, + UPNP, + WSD, + UNKNOWN; + } + + public record HostName(@Nullable String name, Source source) { + } + + protected static record Interface(String name, int hostCount) { + } + + private static record WakeOnLineData(String mac, String password) { + } + + private static enum Type { + MAC_ADDRESS, + UNKNOWN; + } + + private static record L2Ident(MACAddress id, Type type) { + } + + private static record L3Connectivity(String addr, Af af, boolean active, boolean reachable, + ZonedDateTime lastActivity, ZonedDateTime lastTimeReachable, String model) { + + private static enum Af { + IPV4, + IPV6, + UNKNOWN; + } + + public IPAddress getIPAddress() { + if (af != Af.UNKNOWN) { + return new IPAddressString(addr).getAddress(); + } + return NULL_IP; + } + } + + public static record HostIntf(LanHost host, Interface intf) { + } + + private static enum HostType { + WORKSTATION, + LAPTOP, + SMARTPHONE, + TABLET, + PRINTER, + VG_CONSOLE, + TELEVISION, + NAS, + IP_CAMERA, + IP_PHONE, + FREEBOX_PLAYER, + FREEBOX_HD, + FREEBOX_CRYSTAL, + FREEBOX_MINI, + FREEBOX_DELTA, + FREEBOX_ONE, + FREEBOX_WIFI, + FREEBOX_POP, + NETWORKING_DEVICE, + MULTIMEDIA_DEVICE, + CAR, + OTHER, + UNKNOWN; + } + + public static record LanHost(String id, @Nullable String primaryName, HostType hostType, boolean primaryNameManual, + L2Ident l2ident, @Nullable String vendorName, boolean persistent, boolean reachable, + @Nullable ZonedDateTime lastTimeReachable, boolean active, @Nullable ZonedDateTime lastActivity, + @Nullable ZonedDateTime firstActivity, List names, List l3connectivities, + @Nullable LanAccessPoint accessPoint) { + + public @Nullable LanAccessPoint accessPoint() { + return accessPoint; + } + + public String vendorName() { + String localVendor = vendorName; + return localVendor != null ? localVendor : "Unknown"; + } + + public Optional getPrimaryName() { + return Optional.ofNullable(primaryName); + } + + public Optional getUPnPName() { + return names.stream().filter(name -> name.source == Source.UPNP).findFirst().map(name -> name.name); + } + + public MACAddress getMac() { + if (Type.MAC_ADDRESS.equals(l2ident.type)) { + return l2ident.id; + } + throw new IllegalArgumentException("This host does not seem to have a Mac Address. Weird."); + } + + public @Nullable IPAddress getIpv4() { + return l3connectivities.stream().filter(L3Connectivity::reachable).map(L3Connectivity::getIPAddress) + .filter(ip -> !ip.equals(NULL_IP) && ip.isIPv4()).findFirst().orElse(null); + } + + public @Nullable ZonedDateTime getLastSeen() { + ZonedDateTime localLastActivity = lastActivity; + if (lastTimeReachable == null && localLastActivity == null) { + return null; + } + if (lastTimeReachable == null) { + return lastActivity; + } + if (localLastActivity == null) { + return lastTimeReachable; + } else { + return localLastActivity.isAfter(lastTimeReachable) ? lastActivity : lastTimeReachable; + } + } + } + + private final List interfaces = new ArrayList<>(); + + public LanBrowserManager(FreeboxOsSession session, UriBuilder uriBuilder) throws FreeboxException { + super(session, LoginManager.Permission.NONE, InterfacesResponse.class, uriBuilder.path(PATH)); + listSubPath = INTERFACES; + } + + private List getInterfaceHosts(String lanInterface) throws FreeboxException { + return get(HostsResponse.class, lanInterface); + } + + private @Nullable LanHost getHost(String lanInterface, String hostId) throws FreeboxException { + return getSingle(HostsResponse.class, lanInterface, hostId); + } + + // As the list of interfaces on the box may not change, we cache the result + private List getInterfaces() throws FreeboxException { + if (interfaces.isEmpty()) { + interfaces.addAll(getDevices()); + } + return interfaces; + } + + public synchronized List getHosts() throws FreeboxException { + List hosts = new ArrayList<>(); + + for (Interface intf : getInterfaces()) { + hosts.addAll(getInterfaceHosts(intf.name())); + } + return hosts; + } + + public Optional getHost(MACAddress searched) throws FreeboxException { + for (Interface intf : getInterfaces()) { + LanHost host = getHost(intf.name(), "ether-" + searched.toColonDelimitedString()); + if (host != null) { + return Optional.of(new HostIntf(host, intf)); + } + } + return Optional.empty(); + } + + public boolean wakeOnLan(MACAddress mac, String password) throws FreeboxException { + Optional target = getHost(mac); + if (target.isPresent()) { + post(new WakeOnLineData(mac.toColonDelimitedString(), password), GenericResponse.class, WOL_ACTION, + target.get().intf.name); + return true; + } + return false; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanManager.java new file mode 100644 index 00000000000..e628cb2f2f8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LanManager.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +import inet.ipaddr.IPAddress; + +/** + * The {@link LanManager} is the Java class used to handle api requests related to lan + * https://dev.freebox.fr/sdk/os/system/# + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LanManager extends ConfigurableRest { + private static final String PATH = "lan"; + + protected static class Config extends Response { + } + + private static enum Mode { + ROUTER, + BRIDGE, + UNKNOWN; + } + + public static record LanConfig(IPAddress ip, String name, String nameDns, String nameMdns, String nameNetbios, + Mode mode) { + } + + public LanManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, Config.class, session.getUriBuilder().path(PATH), CONFIG_PATH); + session.addManager(LanBrowserManager.class, new LanBrowserManager(session, getUriBuilder())); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LcdManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LcdManager.java new file mode 100644 index 00000000000..b19678f5458 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LcdManager.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.concurrent.Callable; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link LcdManager} is the Java class used to handle api requests related to lcd screen of the server + * https://dev.freebox.fr/sdk/os/system/# + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LcdManager extends ConfigurableRest { + private static final String PATH = "lcd"; + + protected static class ConfigResponse extends Response { + } + + public static record Config(int brightness, int orientation, boolean orientationForced) { + } + + public LcdManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + } + + private void setBrightness(int brightness) throws FreeboxException { + Config oldConfig = getConfig(); + setConfig(new Config(brightness, oldConfig.orientation, oldConfig.orientationForced)); + } + + public void setOrientation(int orientation) throws FreeboxException { + Config oldConfig = getConfig(); + setConfig(new Config(oldConfig.brightness, orientation, oldConfig.orientationForced)); + } + + public void setOrientationForced(boolean forced) throws FreeboxException { + Config oldConfig = getConfig(); + setConfig(new Config(oldConfig.brightness, oldConfig.orientation, forced)); + } + + public void setBrightness(Callable function) throws FreeboxException { + try { + setBrightness(function.call()); + } catch (Exception e) { + throw new FreeboxException(e, "Error setting brightness"); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ListableRest.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ListableRest.java new file mode 100644 index 00000000000..f88b30d89dd --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/ListableRest.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.List; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link ListableRest} is the Java class used to handle rest answers holding a list of known equipments + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ListableRest> extends RestManager { + private final Class deviceResponseClass; + + protected @Nullable String listSubPath = null; + + public ListableRest(FreeboxOsSession session, LoginManager.Permission required, Class respClass, UriBuilder uri) + throws FreeboxException { + super(session, required, uri); + this.deviceResponseClass = respClass; + } + + public List getDevices() throws FreeboxException { + return listSubPath == null ? get(deviceResponseClass) : get(deviceResponseClass, listSubPath); + } + + public T getDevice(int deviceId) throws FreeboxException { + return getSingle(deviceResponseClass, Integer.toString(deviceId)); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LoginManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LoginManager.java new file mode 100644 index 00000000000..41e38e235ad --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/LoginManager.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static javax.xml.bind.DatatypeConverter.printHexBinary; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Optional; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +/** + * The {@link LoginManager} is the Java class used to handle api requests related to session handling and login + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LoginManager extends RestManager { + private static final Bundle BUNDLE = FrameworkUtil.getBundle(LoginManager.class); + private static final String APP_ID = BUNDLE.getSymbolicName(); + private static final String ALGORITHM = "HmacSHA1"; + private static final String PATH = "login"; + private static final String SESSION = "session"; + private static final String AUTHORIZE_ACTION = "authorize"; + private static final String LOGOUT = "logout"; + + private static enum Status { + PENDING, // the user has not confirmed the autorization request yet + TIMEOUT, // the user did not confirmed the authorization within the given time + GRANTED, // the app_token is valid and can be used to open a session + DENIED, // the user denied the authorization request + UNKNOWN; // the app_token is invalid or has been revoked + } + + private static record AuthorizationStatus(Status status, boolean loggedIn, String challenge, + @Nullable String passwordSalt, boolean passwordSet) { + } + + private static class AuthStatus extends Response { + } + + private static record Authorization(String appToken, String trackId) { + } + + private static class AuthResponse extends Response { + } + + public static enum Permission { + PARENTAL, + CONTACTS, + EXPLORER, + TV, + WDO, + DOWNLOADER, + PROFILE, + CAMERA, + SETTINGS, + CALLS, + HOME, + PVR, + VM, + PLAYER, + NONE, + UNKNOWN; + } + + public static record Session(Map permissions, + @Nullable String sessionToken) { + protected boolean hasPermission(LoginManager.Permission checked) { + return Boolean.TRUE.equals(permissions.get(checked)); + } + } + + private static class SessionResponse extends Response { + } + + private static record AuthorizeData(String appId, String appName, String appVersion, String deviceName) { + AuthorizeData(String appId, Bundle bundle) { + this(appId, bundle.getHeaders().get("Bundle-Name"), bundle.getVersion().toString(), + bundle.getHeaders().get("Bundle-Vendor")); + } + } + + private static record OpenSessionData(String appId, String password) { + } + + private final Mac mac; + private Optional authorize = Optional.empty(); + + public LoginManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(PATH)); + try { + this.mac = Mac.getInstance(ALGORITHM); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public Session openSession(String appToken) throws FreeboxException { + AuthorizationStatus authorization = getSingle(AuthStatus.class); + + try { + // Initialize mac with the signing key + mac.init(new SecretKeySpec(appToken.getBytes(), mac.getAlgorithm())); + // Compute the hmac on input data bytes + byte[] rawHmac = mac.doFinal(authorization.challenge().getBytes()); + // Convert raw bytes to Hex + String password = printHexBinary(rawHmac).toLowerCase(); + return post(new OpenSessionData(APP_ID, password), SessionResponse.class, SESSION); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + } + + public void closeSession() throws FreeboxException { + post(LOGOUT); + } + + public String checkGrantStatus() throws FreeboxException { + if (authorize.isEmpty()) { + authorize = Optional.of(post(new AuthorizeData(APP_ID, BUNDLE), AuthResponse.class, AUTHORIZE_ACTION)); + } + + return switch (getSingle(AuthStatus.class, AUTHORIZE_ACTION, authorize.get().trackId).status()) { + case PENDING -> ""; + case GRANTED -> { + String appToken = authorize.get().appToken; + authorize = Optional.empty(); + yield appToken; + } + case TIMEOUT -> throw new FreeboxException("Unable to grant session, delay expired"); + case DENIED -> throw new FreeboxException("Unable to grant session, access was denied"); + case UNKNOWN -> throw new FreeboxException("Unable to grant session"); + }; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/MediaReceiverManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/MediaReceiverManager.java new file mode 100644 index 00000000000..da7849b9e3c --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/MediaReceiverManager.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.Map; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Receiver; + +/** + * The {@link MediaReceiverManager} is the Java class used to handle api requests related to air media receivers + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class MediaReceiverManager extends ListableRest { + private static final String SUB_PATH = "receivers"; + + public static record Receiver(boolean passwordProtected, // + Map capabilities, // + String name // This name is the UPnP name of the host + ) { + } + + protected static class ReceiverResponse extends Response { + } + + public static enum Action { + START, + STOP, + UNKNOWN; + } + + public static enum MediaType { + VIDEO, + PHOTO, + AUDIO, + SCREEN, + UNKNOWN; + } + + private static record Request(String password, Action action, MediaType mediaType, @Nullable String media, + int position) { + } + + public MediaReceiverManager(FreeboxOsSession session, UriBuilder uriBuilder) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ReceiverResponse.class, uriBuilder.path(SUB_PATH)); + } + + public @Nullable Receiver getReceiver(String receiverName) throws FreeboxException { + return getDevices().stream().filter(rcv -> receiverName.equals(rcv.name())).findFirst().orElse(null); + } + + public void sendToReceiver(String receiver, String password, Action action, MediaType type) + throws FreeboxException { + sendToReceiver(receiver, new Request(password, action, type, null, 0)); + } + + public void sendToReceiver(String receiver, String password, Action action, MediaType type, String url) + throws FreeboxException { + sendToReceiver(receiver, new Request(password, action, type, url, 0)); + } + + private void sendToReceiver(String receiver, Request payload) throws FreeboxException { + post(payload, GenericResponse.class, receiver); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/NetShareManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/NetShareManager.java new file mode 100644 index 00000000000..617eac47df0 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/NetShareManager.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; + +/** + * The {@link NetShareManager} is the Java class used to handle api requests related to network shares + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class NetShareManager extends RestManager { + private static final String PATH = "netshare"; + + public NetShareManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(PATH)); + session.addManager(SambaManager.class, new SambaManager(session, getUriBuilder())); + session.addManager(AfpManager.class, new AfpManager(session, getUriBuilder())); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PhoneManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PhoneManager.java new file mode 100644 index 00000000000..dc797a177d3 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PhoneManager.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link PhoneManager} is the Java class used to handle api requests related to phone and calls + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class PhoneManager extends ConfigurableRest { + private static final String DECT_PAGE_ACTION = "dect_page_%s"; + private static final String FXS_RING_ACTION = "fxs_ring_%s"; + private static final String PATH = "phone"; + + protected class ConfigResponse extends Response { + } + + protected class StatusResponse extends Response { + } + + private static enum NetworkStatus { + WORKING, + UNKNOWN; + } + + public static record Config(NetworkStatus network, boolean dectEcoMode, String dectPin, int dectRingPattern, + boolean dectRegistration, boolean dectNemoMode, boolean dectEnabled, boolean dectRingOnOff) { + } + + public enum Type { + FXS, + DECT, + UNKNOWN; + } + + public static record Status(int id, boolean isRinging, boolean onHook, boolean hardwareDefect, Type type, + @Nullable String vendor, int gainRx, int gainTx) { + + public String vendor() { + String localVendor = vendor; + return localVendor != null ? localVendor : "Unknown"; + } + } + + public PhoneManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.CALLS, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + } + + public List getPhoneStatuses() throws FreeboxException { + return get(StatusResponse.class, ""); + } + + public Optional getStatus(int id) throws FreeboxException { + return Optional.ofNullable(getSingle(StatusResponse.class, Integer.toString(id))); + } + + public void ringFxs(boolean startIt) throws FreeboxException { + post(FXS_RING_ACTION.formatted(startIt ? "start" : "stop")); + } + + public void ringDect(boolean startIt) throws FreeboxException { + post(DECT_PAGE_ACTION.formatted(startIt ? "start" : "stop")); + } + + public void setGainRx(int clientId, int gain) throws FreeboxException { + Optional result = getStatus(clientId); + if (result.isPresent()) { + Status status = result.get(); + Status newStatus = new Status(status.id, status.isRinging, status.onHook, status.hardwareDefect, + status.type, status.vendor, gain, status.gainTx); + put(StatusResponse.class, newStatus, Integer.toString(clientId)); + } + } + + public void setGainTx(int clientId, int gain) throws FreeboxException { + Optional result = getStatus(clientId); + if (result.isPresent()) { + Status status = result.get(); + Status newStatus = new Status(status.id, status.isRinging, status.onHook, status.hardwareDefect, + status.type, status.vendor, status.gainRx, gain); + put(StatusResponse.class, newStatus, Integer.toString(clientId)); + } + } + + public void alternateRing(boolean status) throws FreeboxException { + Config config = getConfig(); + Config newConfig = new Config(config.network, config.dectEcoMode, config.dectPin, config.dectRingPattern, + config.dectRegistration, config.dectNemoMode, config.dectEnabled, status); + put(ConfigResponse.class, newConfig, CONFIG_PATH); + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + Config config = getConfig(); + Config newConfig = new Config(config.network, config.dectEcoMode, config.dectPin, config.dectRingPattern, + config.dectRegistration, config.dectNemoMode, enabled, config.dectRingOnOff); + return setConfig(newConfig).dectEnabled; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PlayerManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PlayerManager.java new file mode 100644 index 00000000000..7ef7907ecde --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/PlayerManager.java @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.THING_PLAYER; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Metadata.PlaybackState; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Metadata.SubtitleTrack; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Metadata.VideoTrack; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.PlayerContext.PlayerDetails; +import org.openhab.binding.freeboxos.internal.api.rest.SystemManager.ModelInfo; + +import com.google.gson.annotations.SerializedName; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link PlayerManager} is the Java class used to handle api requests related to player + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class PlayerManager extends ListableRest { + private static final String STATUS_PATH = "status"; + + protected static class PlayerResponse extends Response { + } + + public static enum DeviceModel { + FBX7HD_DELTA, // Freebox Player Devialet + TBX8AM, // Player Pop + FBX6HD, + FBX6LC, + FBX6LCV2, + FBX7HD, + FBX7HD_ONE, + FBX8AM, + UNKNOWN; + } + + public static record Player(MACAddress mac, StbType stbType, int id, ZonedDateTime lastTimeReachable, + boolean apiAvailable, String deviceName, DeviceModel deviceModel, boolean reachable, String uid, + @Nullable String apiVersion, List lanGids) { + private static enum StbType { + STB_ANDROID, + STB_V6, + STB_V7, + STB_V8, + UNKNOWN; + } + + /** + * @return a string like eg: '17/api/v8' + */ + private @Nullable String baseUrl() { + String api = apiVersion; + return api != null ? "%d/api/v%s/".formatted(id, api.split("\\.")[0]) : null; + } + } + + private static class StatusResponse extends Response { + } + + public static enum PowerState { + STANDBY, + RUNNING, + UNKNOWN; + } + + public static record Status(PowerState powerState, StatusInformation player, + @Nullable ForegroundApp foregroundApp) { + + public @Nullable ForegroundApp foregroundApp() { + return foregroundApp; + } + } + + public static record ForegroundApp(int packageId, @Nullable String curlUrl, @Nullable Object context, + @SerializedName(value = "package") String _package) { + } + + private static record StatusInformation(String name, ZonedDateTime lastActivity) { + } + + private static class ConfigurationResponse extends Response { + } + + public static record Configuration(String boardName, boolean configured, String firmwareVersion, + @Nullable ModelInfo modelInfo, String serial, String uptime, long uptimeVal) { + } + + private enum MediaState { + READY, + UNKNOWN; + } + + private static record AudioTrack(int bitrate, @SerializedName("channelCount") int channelCount, + @Nullable String codec, @SerializedName("codecId") @Nullable String codecId, @Nullable String language, + @SerializedName("metadataId") @Nullable String metadataId, int pid, int samplerate, long uid) { + } + + private static enum Type { + NORMAL, + HEARINGIMPAIRED, + UNKNOWN; + } + + protected static record Metadata(@Nullable String album, + @SerializedName("albumArtist") @Nullable String albumArtist, @Nullable String artist, + @Nullable String author, int bpm, @Nullable String comment, boolean compilation, @Nullable String composer, + @Nullable String container, @Nullable String copyright, long date, + @SerializedName("discId") @Nullable String discId, @SerializedName("discNumber") int discNumber, + @SerializedName("discTotal") int discTotal, @Nullable String genre, + @SerializedName("musicbrainzDiscId") @Nullable String musicbrainzDiscId, @Nullable String performer, + @Nullable String title, @SerializedName("trackNumber") int trackNumber, + @SerializedName("trackTotal") int trackTotal, @Nullable String url) { + + protected static enum PlaybackState { + PLAY, + PAUSE, + UNKNOWN; + } + + protected static record SubtitleTrack(@Nullable String codec, @Nullable String language, @Nullable String pid, + Type type, @Nullable String uid) { + } + + protected static record VideoTrack(int bitrate, @Nullable String codec, int height, int pid, int uid, + int width) { + } + } + + public static record PlayerContext(@Nullable PlayerDetails player) { + public static record PlayerDetails(@SerializedName("audioIndex") int audioIndex, + @SerializedName("audioList") List audioList, @SerializedName("curPos") long curPos, + int duration, @SerializedName("livePos") long livePos, @SerializedName("maxPos") long maxPos, + @SerializedName("mediaState") MediaState mediaState, @Nullable Metadata metadata, + @SerializedName("minPos") long minPos, @SerializedName("playbackState") PlaybackState playbackState, + long position, @Nullable String source, @SerializedName("subtitleIndex") int subtitleIndex, + @SerializedName("subtitleList") List subtitleList, + @SerializedName("videoIndex") int videoIndex, @SerializedName("videoList") List videoList) { + } + } + + private static enum BouquetType { + ADSL, + UNKNOWN; + } + + private static enum ChannelType { + REGULAR, + UNKNOWN; + } + + private static record Service(long id, @Nullable String name, + @SerializedName("qualityLabel") @Nullable String qualityLabel, + @SerializedName("qualityName") @Nullable String qualityName, @SerializedName("sortInfo") int sortInfo, + @SerializedName("typeLabel") @Nullable String typeLabel, + @SerializedName("typeName") @Nullable String typeName, @Nullable String url) { + } + + private static record Channel(@SerializedName("bouquetId") long bouquetId, + @SerializedName("bouquetName") @Nullable String bouquetName, + @SerializedName("bouquetType") BouquetType bouquetType, + @SerializedName("channelName") @Nullable String channelName, + @SerializedName("channelNumber") int channelNumber, + @SerializedName("channelSubNumber") int channelSubNumber, + @SerializedName("channelType") ChannelType channelType, + @SerializedName("channelUuid") @Nullable String channelUuid, + @SerializedName("currentServiceIndex") int currentServiceIndex, + @SerializedName("isTimeShifting") boolean isTimeShifting, List services, + @SerializedName("videoIsVisible") boolean videoIsVisible) { + } + + public static record TvContext(@Nullable Channel channel, @Nullable PlayerDetails player) { + } + + private final Map subPaths = new HashMap<>(); + + public PlayerManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.PLAYER, PlayerResponse.class, + session.getUriBuilder().path(THING_PLAYER)); + getDevices().stream().filter(Player::apiAvailable).forEach(player -> { + String baseUrl = player.baseUrl(); + if (baseUrl != null) { + subPaths.put(player.id, baseUrl); + } + }); + } + + public Status getPlayerStatus(int id) throws FreeboxException { + return getSingle(StatusResponse.class, subPaths.get(id), STATUS_PATH); + } + + // The player API does not allow to directly request a given player like others api parts + @Override + public Player getDevice(int id) throws FreeboxException { + return getDevices().stream().filter(player -> player.id == id).findFirst().orElse(null); + } + + public Configuration getConfig(int id) throws FreeboxException { + return getSingle(ConfigurationResponse.class, subPaths.get(id), SYSTEM_PATH); + } + + public void sendKey(String ip, String code, String key, boolean longPress, int count) { + UriBuilder uriBuilder = UriBuilder.fromPath("pub").scheme("http").host(ip).path("remote_control"); + uriBuilder.queryParam("code", code).queryParam("key", key); + if (longPress) { + uriBuilder.queryParam("long", true); + } + if (count > 1) { + uriBuilder.queryParam("repeat", count); + } + try { + session.execute(uriBuilder.build(), HttpMethod.GET, GenericResponse.class, null); + } catch (FreeboxException ignore) { + // This call does not return anything, we can safely ignore + } + } + + public void reboot(int id) throws FreeboxException { + post(subPaths.get(id), SYSTEM_PATH, REBOOT_ACTION); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RepeaterManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RepeaterManager.java new file mode 100644 index 00000000000..d76b4a646a8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RepeaterManager.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.HostsResponse; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link RepeaterManager} is the Java class used to handle api requests related to repeater + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class RepeaterManager extends ListableRest { + + protected static class RepeaterResponse extends Response { + } + + protected static class RepeaterLedResponse extends Response { + } + + public static record RepeaterLed(int id, boolean ledActivated) { + } + + private static enum Connection { + CONNECTED, + DISCONNECTED, + UNKNOWN; + } + + private static enum Status { + STARTING, + RUNNING, + REBOOTING, + UPDATING, + REBOOT_FAILURE, + UPDATE_FAILURE, + UNKNOWN; + } + + public static enum Model { + FBXWMR, // Répéteur Wifi + UNKNOWN; + } + + public static record Repeater(int id, boolean ledActivated, boolean enabled, MACAddress mainMac, + Connection connection, ZonedDateTime bootTime, Status status, String name, String sn, String apiVer, + ZonedDateTime lastSeen, Model model, String firmwareVersion) { + + public long getUptimeVal() { + return Duration.between(bootTime, ZonedDateTime.now()).toSeconds(); + } + } + + public RepeaterManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, RepeaterResponse.class, + session.getUriBuilder().path(THING_REPEATER)); + } + + public List getRepeaterHosts(int id) throws FreeboxException { + return get(HostsResponse.class, Integer.toString(id), THING_HOST); + } + + public synchronized List getHosts() throws FreeboxException { + List hosts = new ArrayList<>(); + for (Repeater rep : getDevices()) { + if (Connection.CONNECTED.equals(rep.connection)) { + hosts.addAll(getRepeaterHosts(rep.id)); + } + } + return hosts; + } + + public Optional getHost(MACAddress mac) throws FreeboxException { + return getHosts().stream().filter(host -> host.getMac().equals(mac)).findFirst(); + } + + public void reboot(int id) throws FreeboxException { + post(Integer.toString(id), REBOOT_ACTION); + } + + public Optional led(int id, boolean enable) throws FreeboxException { + RepeaterLed result = put(RepeaterLedResponse.class, new RepeaterLed(id, enable), Integer.toString(id)); + return Optional.ofNullable(result); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RestManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RestManager.java new file mode 100644 index 00000000000..bb1a57c279d --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/RestManager.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.eclipse.jetty.http.HttpMethod.*; + +import java.net.URI; +import java.util.List; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.PermissionException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * Base class for the various rest managers available through the API + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class RestManager { + protected static final String REBOOT_ACTION = "reboot"; + protected static final String SYSTEM_PATH = "system"; + + protected class GenericResponse extends Response { + } + + private final UriBuilder uriBuilder; + protected final FreeboxOsSession session; + + public RestManager(FreeboxOsSession session, LoginManager.Permission required, UriBuilder uri) + throws FreeboxException { + this.uriBuilder = uri; + this.session = session; + if (required != LoginManager.Permission.NONE && !session.hasPermission(required)) { + throw new PermissionException(required, "Permission missing: %s", required.toString()); + } + } + + protected UriBuilder getUriBuilder() { + return uriBuilder.clone(); + } + + private URI buildUri(String... pathElements) { + UriBuilder localBuilder = getUriBuilder(); + for (String path : pathElements) { + localBuilder.path(path); + } + return localBuilder.build(); + } + + // Returns the first and supposed only element from the list. Presence of this element is expected and mandatory + private F controlSingleton(List result) { + if (result.size() == 1) { + return result.get(0); + } + throw new IllegalArgumentException("Result is empty or not singleton"); + } + + protected > List get(Class clazz, String... pathElements) throws FreeboxException { + return session.execute(buildUri(pathElements), GET, clazz, null); + } + + protected > F getSingle(Class clazz, String... pathElements) throws FreeboxException { + return controlSingleton(get(clazz, pathElements)); + } + + protected > F post(Object payload, Class clazz, String... pathElements) + throws FreeboxException { + return controlSingleton(session.execute(buildUri(pathElements), POST, clazz, payload)); + } + + protected void post(String... pathElements) throws FreeboxException { + session.execute(buildUri(pathElements), POST, GenericResponse.class, null); + } + + protected > F put(Class clazz, F payload, String... pathElements) + throws FreeboxException { + return controlSingleton(session.execute(buildUri(pathElements), PUT, clazz, payload)); + } + + protected > void put(F payload, String... pathElements) throws FreeboxException { + session.execute(buildUri(pathElements), PUT, GenericResponse.class, payload); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SambaManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SambaManager.java new file mode 100644 index 00000000000..f947977eec3 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SambaManager.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link SambaManager} is the Java class used to handle api requests related to Samba shares + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class SambaManager extends ConfigurableRest { + private static final String PATH = "samba"; + + protected static class ConfigResponse extends Response { + } + + public static record Samba(boolean fileShareEnabled, boolean printShareEnabled, boolean logonEnabled, + @Nullable String logonUser, @Nullable String logonPassword, @Nullable String workgroup, + boolean smbv2Enabled) { + } + + public SambaManager(FreeboxOsSession session, UriBuilder uriBuilder) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, uriBuilder.path(PATH), null); + } + + public boolean setFileShare(boolean enable) throws FreeboxException { + Samba config = getConfig(); + Samba newConfig = new Samba(enable, config.printShareEnabled, config.logonEnabled, config.logonUser, + config.logonPassword, config.workgroup, config.smbv2Enabled); + return setConfig(newConfig).fileShareEnabled(); + } + + public boolean setPrintShare(boolean enable) throws FreeboxException { + Samba config = getConfig(); + Samba newConfig = new Samba(config.fileShareEnabled, enable, config.logonEnabled, config.logonUser, + config.logonPassword, config.workgroup, config.smbv2Enabled); + return setConfig(newConfig).printShareEnabled(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SystemManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SystemManager.java new file mode 100644 index 00000000000..2ed5b0a6636 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/SystemManager.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; +import org.openhab.binding.freeboxos.internal.api.rest.FreeboxOsSession.BoxModel; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link SystemManager} is the Java class used to handle api requests related to system + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class SystemManager extends ConfigurableRest { + + protected static class ConfigurationResponse extends Response { + } + + public static record Sensor(String id, String name, int value) { + public enum SensorKind { + FAN, + TEMP, + UNKNOWN; + } + + public SensorKind getKind() { + String[] elements = id.split("_"); + if (elements.length > 0) { + String kind = elements[0].replaceAll("\\d", "").toUpperCase(); + try { + return SensorKind.valueOf(kind); + } catch (IllegalArgumentException ignore) { // returning UNKNOWN + } + } + return SensorKind.UNKNOWN; + } + } + + private static record Expansion(int slot, boolean probeDone, boolean present, boolean supported, String bundle, + Type type) { + private static enum Type { + UNKNOWN, // unknown module + DSL_LTE, // xDSL + LTE + DSL_LTE_EXTERNAL_ANTENNAS, // xDSL + LTE with external antennas switch + FTTH_P2P, // FTTH P2P + FTTH_PON, // FTTH PON + SECURITY; // Security module + } + } + + public static record ModelInfo(BoxModel name, String prettyName, boolean hasExpansions, boolean hasLanSfp, + boolean hasDect, boolean hasHomeAutomation, boolean hasFemtocellExp, boolean hasFixedFemtocell, + boolean hasVm) { + } + + public static record Config(String firmwareVersion, MACAddress mac, String serial, String uptime, long uptimeVal, + String boardName, boolean boxAuthenticated, DiskStatus diskStatus, String userMainStorage, + List sensors, ModelInfo modelInfo, List fans, List expansions) { + private static enum DiskStatus { + NOT_DETECTED, + DISABLED, + INITIALIZING, + ERROR, + ACTIVE, + UNKNOWN; + } + } + + public SystemManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigurationResponse.class, + session.getUriBuilder().path(SYSTEM_PATH), null); + } + + public void reboot() throws FreeboxException { + post(REBOOT_ACTION); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/UPnPAVManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/UPnPAVManager.java new file mode 100644 index 00000000000..035df9a9923 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/UPnPAVManager.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link UPnPAVManager} is the Java class used to handle api requests related to UPnP AV + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class UPnPAVManager extends ConfigurableRest { + private static final String PATH = "upnpav"; + + protected static class ConfigResponse extends Response { + } + + protected static record Config(boolean enabled) { + } + + public UPnPAVManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + } + + public boolean getStatus() throws FreeboxException { + return getConfig().enabled(); + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + return setConfig(new Config(enabled)).enabled(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/VmManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/VmManager.java new file mode 100644 index 00000000000..07b75d6ac17 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/VmManager.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.THING_VM; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link VmManager} is the Java class used to handle api requests related to virtual machines + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class VmManager extends ListableRest { + + protected class VirtualMachineResponse extends Response { + } + + public static enum Status { + STOPPED, + RUNNING, + UNKNOWN; + } + + public static record VirtualMachine(int id, String name, MACAddress mac, Status status) { + } + + public VmManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.VM, VirtualMachineResponse.class, + session.getUriBuilder().path(THING_VM)); + } + + public void power(int vmId, boolean startIt) throws FreeboxException { + post(Integer.toString(vmId), startIt ? "start" : "powerbutton"); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java new file mode 100644 index 00000000000..9607c7b6a46 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WebSocketManager.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.freeboxos.internal.api.ApiHandler; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine; +import org.openhab.binding.freeboxos.internal.handler.HostHandler; +import org.openhab.binding.freeboxos.internal.handler.VmHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link WebSocketManager} is the Java class register to the websocket server and handle notifications + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class WebSocketManager extends RestManager implements WebSocketListener { + private static final String HOST_UNREACHABLE = "lan_host_l3addr_unreachable"; + private static final String HOST_REACHABLE = "lan_host_l3addr_reachable"; + private static final String VM_CHANGED = "vm_state_changed"; + private static final Register REGISTRATION = new Register("register", + List.of(VM_CHANGED, HOST_REACHABLE, HOST_UNREACHABLE)); + private static final String WS_PATH = "ws/event"; + + private final Logger logger = LoggerFactory.getLogger(WebSocketManager.class); + private final Map lanHosts = new HashMap<>(); + private final Map vms = new HashMap<>(); + private final ApiHandler apiHandler; + + private volatile @Nullable Session wsSession; + + private record Register(String action, List events) { + + } + + public WebSocketManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, session.getUriBuilder().path(WS_PATH)); + this.apiHandler = session.getApiHandler(); + } + + private static enum Action { + REGISTER, + NOTIFICATION, + UNKNOWN; + } + + private static record WebSocketResponse(boolean success, Action action, String event, String source, + @Nullable JsonElement result) { + public String getEvent() { + return source + "_" + event; + } + } + + public void openSession(@Nullable String sessionToken) throws FreeboxException { + WebSocketClient client = new WebSocketClient(apiHandler.getHttpClient()); + URI uri = getUriBuilder().scheme(getUriBuilder().build().getScheme().contains("s") ? "wss" : "ws").build(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setHeader(ApiHandler.AUTH_HEADER, sessionToken); + + try { + client.start(); + client.connect(this, uri, request); + } catch (Exception e) { + throw new FreeboxException(e, "Exception connecting websocket client"); + } + } + + public void closeSession() { + logger.debug("Awaiting closure from remote"); + Session localSession = wsSession; + if (localSession != null) { + localSession.close(); + } + } + + @Override + public void onWebSocketConnect(@NonNullByDefault({}) Session wsSession) { + this.wsSession = wsSession; + logger.debug("Websocket connection establisehd"); + try { + wsSession.getRemote().sendString(apiHandler.serialize(REGISTRATION)); + } catch (IOException e) { + logger.warn("Error connecting to websocket: {}", e.getMessage()); + } + } + + @Override + public void onWebSocketText(@NonNullByDefault({}) String message) { + Session localSession = wsSession; + if (message.toLowerCase(Locale.US).contains("bye") && localSession != null) { + localSession.close(StatusCode.NORMAL, "Thanks"); + return; + } + + WebSocketResponse result = apiHandler.deserialize(WebSocketResponse.class, message); + if (result.success) { + switch (result.action) { + case REGISTER: + logger.debug("Event registration successfull"); + break; + case NOTIFICATION: + handleNotification(result); + break; + default: + logger.warn("Unhandled notification received: {}", result.action); + } + } + } + + private void handleNotification(WebSocketResponse result) { + JsonElement json = result.result; + if (json != null) { + switch (result.getEvent()) { + case VM_CHANGED: + VirtualMachine vm = apiHandler.deserialize(VirtualMachine.class, json.toString()); + logger.debug("Received notification for VM {}", vm.id()); + VmHandler vmHandler = vms.get(vm.id()); + if (vmHandler != null) { + vmHandler.updateVmChannels(vm); + } + break; + case HOST_UNREACHABLE, HOST_REACHABLE: + LanHost host = apiHandler.deserialize(LanHost.class, json.toString()); + MACAddress mac = host.getMac(); + logger.debug("Received notification for LanHost {}", mac.toColonDelimitedString()); + HostHandler hostHandler = lanHosts.get(mac); + if (hostHandler != null) { + hostHandler.updateConnectivityChannels(host); + } + break; + default: + logger.warn("Unhandled event received: {}", result.getEvent()); + } + } else { + logger.warn("Empty json element in notification"); + } + } + + @Override + public void onWebSocketClose(int statusCode, @NonNullByDefault({}) String reason) { + logger.debug("Socket Closed: [{}] - reason {}", statusCode, reason); + this.wsSession = null; + } + + @Override + public void onWebSocketError(@NonNullByDefault({}) Throwable cause) { + logger.warn("Error on websocket: {}", cause.getMessage()); + } + + @Override + public void onWebSocketBinary(byte @Nullable [] payload, int offset, int len) { + /* do nothing */ + } + + public void registerListener(MACAddress mac, HostHandler hostHandler) { + lanHosts.put(mac, hostHandler); + } + + public void unregisterListener(MACAddress mac) { + lanHosts.remove(mac); + } + + public void registerVm(int clientId, VmHandler vmHandler) { + vms.put(clientId, vmHandler); + } + + public void unregisterVm(int clientId) { + vms.remove(clientId); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WifiManager.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WifiManager.java new file mode 100644 index 00000000000..d5a5f093202 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/api/rest/WifiManager.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.api.rest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.Response; + +/** + * The {@link WifiManager} is the Java class used to handle api requests related to wifi + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class WifiManager extends ConfigurableRest { + private static final String PATH = "wifi"; + + protected static class ConfigResponse extends Response { + } + + protected static record Config(boolean enabled) { + } + + public WifiManager(FreeboxOsSession session) throws FreeboxException { + super(session, LoginManager.Permission.NONE, ConfigResponse.class, session.getUriBuilder().path(PATH), + CONFIG_PATH); + session.addManager(APManager.class, new APManager(session, getUriBuilder())); + } + + public boolean getStatus() throws FreeboxException { + return getConfig().enabled(); + } + + public boolean setStatus(boolean enabled) throws FreeboxException { + return setConfig(new Config(enabled)).enabled(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ApiConsumerConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ApiConsumerConfiguration.java new file mode 100644 index 00000000000..2435ccdb8c5 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ApiConsumerConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ApiConsumerConfiguration { + public static final String REFRESH_INTERVAL = "refreshInterval"; + + public int refreshInterval = 30; + public String password = ""; + public boolean acceptAllMp3 = true; +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ClientConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ClientConfiguration.java new file mode 100644 index 00000000000..116375e171f --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/ClientConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ClientConfiguration} is responsible for holding configuration informations for a controllable client of + * the API + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ClientConfiguration extends HostConfiguration { + public static final String ID = "id"; + + public int id = 1; +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java new file mode 100644 index 00000000000..b079080ddf8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeboxOsConfiguration.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import javax.ws.rs.core.UriBuilder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxTlsCertificateProvider; + +/** + * The {@link FreeboxOsConfiguration} is responsible for holding configuration informations needed to access the Freebox + * API + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxOsConfiguration { + public static final String API_DOMAIN = "apiDomain"; + public static final String APP_TOKEN = "appToken"; + public static final String HTTPS_PORT = "httpsPort"; + public static final String HTTPS_AVAILABLE = "httpsAvailable"; + + private String apiDomain = FreeboxTlsCertificateProvider.DEFAULT_NAME; + public String appToken = ""; + public boolean discoverNetDevice; + + private int httpsPort = 15682; + private boolean httpsAvailable; + + private String getScheme() { + return httpsAvailable ? "https" : "http"; + } + + private int getPort() { + return httpsAvailable ? httpsPort : 80; + } + + public UriBuilder getUriBuilder(String path) { + return UriBuilder.fromPath("/").scheme(getScheme()).port(getPort()).host(apiDomain).path(path).clone(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeplugConfigurationBuilder.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeplugConfigurationBuilder.java new file mode 100644 index 00000000000..0de2a2f02f4 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/FreeplugConfigurationBuilder.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.FreeplugManager.Freeplug; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link FreeplugConfigurationBuilder} is responsible for holding configuration informations associated to a + * Freeplug + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeplugConfigurationBuilder { + private static final FreeplugConfigurationBuilder BUILDER_INSTANCE = new FreeplugConfigurationBuilder(); + + private final Logger logger = LoggerFactory.getLogger(FreeplugConfigurationBuilder.class); + + private FreeplugConfigurationBuilder() { + } + + public static FreeplugConfigurationBuilder getInstance() { + return BUILDER_INSTANCE; + } + + public DiscoveryResultBuilder configure(ThingUID bridgeUID, Freeplug plug) { + MACAddress mac = plug.id(); + String uid = mac.toHexString(false); + ThingUID thingUID = new ThingUID(THING_TYPE_FREEPLUG, bridgeUID, uid); + + logger.debug("Adding new {} {} to inbox", THING_FREEPLUG, thingUID); + + return DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS) + .withLabel("%s (%s) %s".formatted(THING_FREEPLUG, plug.netRole().name(), uid)) + .withProperty(Thing.PROPERTY_MAC_ADDRESS, mac.toColonDelimitedString()); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/HostConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/HostConfiguration.java new file mode 100644 index 00000000000..c69194e14a0 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/HostConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import inet.ipaddr.MACAddressString; +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link HostConfiguration} is responsible for holding + * configuration informations associated to a Freebox Network Device + * thing type + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class HostConfiguration extends ApiConsumerConfiguration { + private String macAddress = ""; + + public MACAddress getMac() { + return new MACAddressString(macAddress).getAddress(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/LandlineConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/LandlineConfiguration.java new file mode 100644 index 00000000000..8c80cb1c9cf --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/LandlineConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LandlineConfiguration} is responsible for holding + * configuration informations associated to a Freebox Phone thing type + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class LandlineConfiguration extends ApiConsumerConfiguration { + public int id = 1; + + LandlineConfiguration() { + refreshInterval = 2; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/NodeConfigurationBuilder.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/NodeConfigurationBuilder.java new file mode 100644 index 00000000000..9aedc880beb --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/NodeConfigurationBuilder.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Category; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.HomeNode; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; + +/** + * The {@link NodeConfigurationBuilder} is responsible for holding configuration informations associated to a Freebox + * Home thing type + * + * @author ben12 - Initial contribution + */ +@NonNullByDefault +public class NodeConfigurationBuilder { + private static final NodeConfigurationBuilder BUILDER_INSTANCE = new NodeConfigurationBuilder(); + + private NodeConfigurationBuilder() { + } + + public static NodeConfigurationBuilder getInstance() { + return BUILDER_INSTANCE; + } + + public Optional configure(ThingUID bridgeUID, HomeNode node) { + if (node.category() == Category.UNKNOWN) { + return Optional.empty(); + } + ThingUID thingUID = new ThingUID(node.category().getThingTypeUID(), bridgeUID, Integer.toString(node.id())); + DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(thingUID); + discoveryResultBuilder.withProperty(ClientConfiguration.ID, node.id()).withLabel(node.label()) + .withRepresentationProperty(ClientConfiguration.ID).withBridge(bridgeUID); + return Optional.of(discoveryResultBuilder); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PhoneConfigurationBuilder.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PhoneConfigurationBuilder.java new file mode 100644 index 00000000000..226171bb108 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PhoneConfigurationBuilder.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Status; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Type; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PhoneConfigurationBuilder} is responsible for holding configuration informations associated the phone + * lines (DECT and FXS / landline) + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class PhoneConfigurationBuilder { + private static final PhoneConfigurationBuilder BUILDER_INSTANCE = new PhoneConfigurationBuilder(); + + private final Logger logger = LoggerFactory.getLogger(PhoneConfigurationBuilder.class); + + private PhoneConfigurationBuilder() { + } + + public static PhoneConfigurationBuilder getInstance() { + return BUILDER_INSTANCE; + } + + public DiscoveryResultBuilder configure(ThingUID bridgeUID, Status config) { + ThingUID thingUID = new ThingUID(Type.DECT.equals(config.type()) ? THING_TYPE_DECT : THING_TYPE_FXS, bridgeUID, + Integer.toString(config.id())); + + logger.debug("Adding new Freebox Phone {} to inbox", thingUID); + + return DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withProperty(ClientConfiguration.ID, config.id()).withLabel(config.type().name()) + .withRepresentationProperty(ClientConfiguration.ID); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PlayerConfiguration.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PlayerConfiguration.java new file mode 100644 index 00000000000..d67a7356b65 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/config/PlayerConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link PlayerConfiguration} is responsible for holding configuration informations needed to access/poll the + * freebox player + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class PlayerConfiguration extends ClientConfiguration { + public static final String REMOTE_CODE = "remoteCode"; + public String remoteCode = ""; +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/console/FreeboxOsCommandExtension.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/console/FreeboxOsCommandExtension.java new file mode 100644 index 00000000000..3303d95b236 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/console/FreeboxOsCommandExtension.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.console; + +import static org.openhab.binding.freeboxos.internal.config.FreeboxOsConfiguration.APP_TOKEN; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants; +import org.openhab.binding.freeboxos.internal.handler.FreeboxOsHandler; +import org.openhab.core.io.console.Console; +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.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link FreeboxOsCommandExtension} is responsible for handling console commands + * + * @author Gaël L'hopital - Initial contribution + */ + +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class FreeboxOsCommandExtension extends AbstractConsoleCommandExtension { + + private final ThingRegistry thingRegistry; + + @Activate + public FreeboxOsCommandExtension(final @Reference ThingRegistry thingRegistry) { + super(FreeboxOsBindingConstants.BINDING_ID, "Interact with the Freebox OS binding."); + this.thingRegistry = thingRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length == 2) { + Thing thing = null; + try { + ThingUID thingUID = new ThingUID(args[0]); + thing = thingRegistry.get(thingUID); + } catch (IllegalArgumentException e) { + thing = null; + } + ThingHandler thingHandler = null; + FreeboxOsHandler handler = null; + if (thing != null) { + thingHandler = thing.getHandler(); + if (thingHandler instanceof FreeboxOsHandler) { + handler = (FreeboxOsHandler) thingHandler; + } + } + if (thing == null) { + console.println("Bad thing id '" + args[0] + "'"); + printUsage(console); + } else if (thingHandler == null) { + console.println("No handler initialized for the thing id '" + args[0] + "'"); + printUsage(console); + } else if (handler == null) { + console.println("'" + args[0] + "' is not a freebox bridge id"); + printUsage(console); + } else { + switch (args[1]) { + case APP_TOKEN: + String token = handler.getConfiguration().appToken; + console.println("Your application token is " + (token.isEmpty() ? "undefined" : token)); + break; + default: + printUsage(console); + break; + } + } + } else { + printUsage(console); + } + } + + @Override + public List getUsages() { + return Arrays.asList(buildCommandUsage(String.format(" %s show the application token", APP_TOKEN))); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/ApiDiscoveryParticipant.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/ApiDiscoveryParticipant.java new file mode 100644 index 00000000000..0f3624c8ca3 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/ApiDiscoveryParticipant.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.discovery; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; +import static org.openhab.binding.freeboxos.internal.config.FreeboxOsConfiguration.*; + +import java.util.Set; + +import javax.jmdns.ServiceInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ApiDiscoveryParticipant} is responsible for discovering the various servers flavors of bridges thing using + * mDNS discovery service + * + * @author Gaël L'hopital - Initial contribution + */ +@Component +@NonNullByDefault +public class ApiDiscoveryParticipant implements MDNSDiscoveryParticipant { + private static final String DOMAIN_PROPERTY = "api_domain"; + private static final String PORT_PROPERTY = "https_port"; + private static final String HTTPS_PROPERTY = "https_available"; + + private final Logger logger = LoggerFactory.getLogger(ApiDiscoveryParticipant.class); + + @Override + public Set getSupportedThingTypeUIDs() { + return BRIDGE_TYPE_UIDS; + } + + @Override + public String getServiceType() { + return "_fbx-api._tcp.local."; + } + + @Override + public @Nullable DiscoveryResult createResult(ServiceInfo service) { + logger.debug("createResult ServiceInfo: {}", service); + ThingUID thingUID = getThingUID(service); + return thingUID != null + ? DiscoveryResultBuilder.create(thingUID).withLabel("Bridge Freebox OS") + .withRepresentationProperty(API_DOMAIN) + .withProperty(HTTPS_AVAILABLE, "1".equals(service.getPropertyString(HTTPS_PROPERTY))) + .withProperty(HTTPS_PORT, service.getPropertyString(PORT_PROPERTY)) + .withProperty(API_DOMAIN, service.getPropertyString(DOMAIN_PROPERTY)).build() + : null; + } + + @Override + public @Nullable ThingUID getThingUID(ServiceInfo service) { + String domain = service.getPropertyString(DOMAIN_PROPERTY); + if (domain != null) { + String[] elements = domain.split("\\."); + if (elements.length > 0) { + return new ThingUID(BRIDGE_TYPE_API, elements[0]); + } + } + return null; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/FreeboxOsDiscoveryService.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/FreeboxOsDiscoveryService.java new file mode 100644 index 00000000000..f81b2dc18c8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/discovery/FreeboxOsDiscoveryService.java @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.discovery; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +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.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.PermissionException; +import org.openhab.binding.freeboxos.internal.api.rest.APManager; +import org.openhab.binding.freeboxos.internal.api.rest.APManager.Station; +import org.openhab.binding.freeboxos.internal.api.rest.FreeplugManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Status; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Player; +import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager; +import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager.Repeater; +import org.openhab.binding.freeboxos.internal.api.rest.SystemManager; +import org.openhab.binding.freeboxos.internal.api.rest.SystemManager.Config; +import org.openhab.binding.freeboxos.internal.api.rest.VmManager; +import org.openhab.binding.freeboxos.internal.config.ClientConfiguration; +import org.openhab.binding.freeboxos.internal.config.FreeplugConfigurationBuilder; +import org.openhab.binding.freeboxos.internal.config.NodeConfigurationBuilder; +import org.openhab.binding.freeboxos.internal.config.PhoneConfigurationBuilder; +import org.openhab.binding.freeboxos.internal.handler.FreeboxOsHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link FreeboxOsDiscoveryService} is responsible for discovering all things + * except the Freebox API thing itself + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxOsDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + private static final int DISCOVERY_TIME_SECONDS = 10; + private static final int BACKGROUND_SCAN_REFRESH_MINUTES = 1; + + private final Logger logger = LoggerFactory.getLogger(FreeboxOsDiscoveryService.class); + + private Optional> backgroundFuture = Optional.empty(); + private @Nullable FreeboxOsHandler bridgeHandler; + + public FreeboxOsDiscoveryService() { + super(Stream.of(THINGS_TYPES_UIDS, HOME_TYPES_UIDS).flatMap(Set::stream).collect(Collectors.toSet()), + DISCOVERY_TIME_SECONDS); + } + + @Override + public void deactivate() { + super.deactivate(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof FreeboxOsHandler) { + bridgeHandler = (FreeboxOsHandler) handler; + activate(null); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + protected void startBackgroundDiscovery() { + stopBackgroundDiscovery(); + backgroundFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::startScan, + BACKGROUND_SCAN_REFRESH_MINUTES, BACKGROUND_SCAN_REFRESH_MINUTES, TimeUnit.MINUTES)); + } + + @Override + protected void stopBackgroundDiscovery() { + backgroundFuture.ifPresent(future -> future.cancel(true)); + backgroundFuture = Optional.empty(); + } + + @Override + protected void startScan() { + logger.debug("Starting Freebox discovery scan"); + FreeboxOsHandler localHandler = bridgeHandler; + if (localHandler != null && localHandler.getThing().getStatus() == ThingStatus.ONLINE) { + try { + ThingUID bridgeUID = localHandler.getThing().getUID(); + + List lanHosts = localHandler.getManager(LanBrowserManager.class).getHosts().stream() + .filter(LanHost::reachable).collect(Collectors.toList()); + + discoverServer(localHandler.getManager(SystemManager.class), bridgeUID); + discoverPhone(localHandler.getManager(PhoneManager.class), bridgeUID); + discoverPlugs(localHandler.getManager(FreeplugManager.class), bridgeUID); + discoverRepeater(localHandler.getManager(RepeaterManager.class), bridgeUID, lanHosts); + discoverPlayer(localHandler.getManager(PlayerManager.class), bridgeUID, lanHosts); + discoverVM(localHandler.getManager(VmManager.class), bridgeUID, lanHosts); + discoverHome(localHandler.getManager(HomeManager.class), bridgeUID); + if (localHandler.getConfiguration().discoverNetDevice) { + discoverHosts(localHandler, bridgeUID, lanHosts); + } + } catch (FreeboxException e) { + logger.warn("Error while requesting data for things discovery: {}", e.getMessage()); + } + } + } + + private void discoverHome(HomeManager homeManager, ThingUID bridgeUID) throws FreeboxException { + NodeConfigurationBuilder builder = NodeConfigurationBuilder.getInstance(); + try { + homeManager.getHomeNodes().forEach( + node -> builder.configure(bridgeUID, node).ifPresent(result -> thingDiscovered(result.build()))); + } catch (PermissionException e) { + logger.warn("Missing permission to discover Home {}", e.getPermission()); + } + } + + private void discoverPlugs(FreeplugManager freeplugManager, ThingUID bridgeUID) { + FreeplugConfigurationBuilder builder = FreeplugConfigurationBuilder.getInstance(); + try { + freeplugManager.getPlugs().forEach(plug -> thingDiscovered(builder.configure(bridgeUID, plug).build())); + } catch (FreeboxException e) { + logger.warn("Error discovering freeplugs {}", e.getMessage()); + } + } + + private void discoverPhone(PhoneManager phoneManager, ThingUID bridgeUID) throws FreeboxException { + PhoneConfigurationBuilder builder = PhoneConfigurationBuilder.getInstance(); + List statuses = List.of(); + try { + statuses = phoneManager.getPhoneStatuses(); + statuses.forEach(phone -> thingDiscovered(builder.configure(bridgeUID, phone).build())); + } catch (FreeboxException e) { + logger.warn("Error discovering phones {}", e.getMessage()); + } + if (!statuses.isEmpty()) { + ThingUID thingUID = new ThingUID(THING_TYPE_CALL, bridgeUID, "landline"); + logger.debug("Adding new Call thing {} to inbox", thingUID); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withLabel("Freebox Calls").build(); + thingDiscovered(discoveryResult); + } + } + + private void discoverHosts(FreeboxOsHandler localHandler, ThingUID bridgeUID, List lanHosts) + throws FreeboxException { + try { + List wifiMacs = new ArrayList<>(); + wifiMacs.addAll(localHandler.getManager(APManager.class).getStations().stream().map(Station::mac) + .collect(Collectors.toList())); + wifiMacs.addAll(localHandler.getManager(RepeaterManager.class).getHosts().stream().map(LanHost::getMac) + .collect(Collectors.toList())); + + lanHosts.forEach(lanHost -> { + MACAddress mac = lanHost.getMac(); + String macString = mac.toColonDelimitedString(); + ThingUID thingUID = new ThingUID(wifiMacs.contains(mac) ? THING_TYPE_WIFI_HOST : THING_TYPE_HOST, + bridgeUID, mac.toHexString(false)); + logger.debug("Adding new Freebox Network Host {} to inbox", thingUID); + DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withLabel(lanHost.getPrimaryName().orElse("Network Device %s".formatted(macString))) + .withTTL(300).withProperty(Thing.PROPERTY_MAC_ADDRESS, macString) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS); + thingDiscovered(builder.build()); + }); + } catch (PermissionException e) { + logger.warn("Missing permission to discover Hosts {}", e.getPermission()); + } + } + + private void discoverVM(VmManager vmManager, ThingUID bridgeUID, List lanHosts) throws FreeboxException { + try { + vmManager.getDevices().forEach(vm -> { + MACAddress mac = vm.mac(); + lanHosts.removeIf(host -> host.getMac().equals(mac)); + + ThingUID thingUID = new ThingUID(THING_TYPE_VM, bridgeUID, mac.toHexString(false)); + logger.debug("Adding new VM Device {} to inbox", thingUID); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS) + .withLabel("%s (VM)".formatted(vm.name())).withProperty(ClientConfiguration.ID, vm.id()) + .withProperty(Thing.PROPERTY_MAC_ADDRESS, mac.toColonDelimitedString()).build(); + thingDiscovered(discoveryResult); + }); + } catch (PermissionException e) { + logger.warn("Missing permission to discover VM {}", e.getPermission()); + } + } + + private void discoverRepeater(RepeaterManager repeaterManager, ThingUID bridgeUID, List lanHosts) + throws FreeboxException { + try { + List repeaters = repeaterManager.getDevices(); + repeaters.forEach(repeater -> { + MACAddress mac = repeater.mainMac(); + lanHosts.removeIf(host -> host.getMac().equals(mac)); + + ThingUID thingUID = new ThingUID(THING_TYPE_REPEATER, bridgeUID, Integer.toString(repeater.id())); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withLabel("Repeater %s".formatted(repeater.name())) + .withProperty(Thing.PROPERTY_MAC_ADDRESS, mac.toColonDelimitedString()) + .withProperty(ClientConfiguration.ID, repeater.id()) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build(); + thingDiscovered(discoveryResult); + }); + } catch (PermissionException e) { + logger.warn("Missing permission to discover Repeater {}", e.getPermission()); + } + } + + private void discoverServer(SystemManager systemManager, ThingUID bridgeUID) throws FreeboxException { + try { + Config config = systemManager.getConfig(); + + ThingTypeUID targetType = config.boardName().startsWith("fbxgw7") ? THING_TYPE_DELTA + : THING_TYPE_REVOLUTION; + ThingUID thingUID = new ThingUID(targetType, bridgeUID, config.serial()); + logger.debug("Adding new Freebox Server {} to inbox", thingUID); + + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withProperty(Thing.PROPERTY_MAC_ADDRESS, config.mac()) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(config.modelInfo().prettyName()) + .build(); + thingDiscovered(discoveryResult); + } catch (PermissionException e) { + logger.warn("Missing permission to discover Server {}", e.getPermission()); + } + } + + private void discoverPlayer(PlayerManager playerManager, ThingUID bridgeUID, List lanHosts) + throws FreeboxException { + try { + for (Player player : playerManager.getDevices()) { + lanHosts.removeIf(host -> host.getMac().equals(player.mac())); + ThingUID thingUID = new ThingUID(player.apiAvailable() ? THING_TYPE_ACTIVE_PLAYER : THING_TYPE_PLAYER, + bridgeUID, Integer.toString(player.id())); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID) + .withLabel(player.deviceName()) + .withProperty(Thing.PROPERTY_MAC_ADDRESS, player.mac().toColonDelimitedString()) + .withProperty(ClientConfiguration.ID, player.id()) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build(); + thingDiscovered(discoveryResult); + } + } catch (PermissionException e) { + logger.warn("Missing permission to discover Player {}", e.getPermission()); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ActivePlayerHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ActivePlayerHandler.java new file mode 100644 index 00000000000..bc7d7812bb6 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ActivePlayerHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.ActivePlayerActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Configuration; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.ForegroundApp; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Player; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Status; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ActivePlayerHandler} is responsible for handling everything associated to Freebox Player with api + * capabilities. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ActivePlayerHandler extends PlayerHandler implements FreeDeviceIntf { + private final Logger logger = LoggerFactory.getLogger(ActivePlayerHandler.class); + + private final ChannelUID eventChannelUID; + + private long uptime = -1; + + public ActivePlayerHandler(Thing thing) { + super(thing); + eventChannelUID = new ChannelUID(getThing().getUID(), SYS_INFO, BOX_EVENT); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + super.initializeProperties(properties); + Player player = getManager(PlayerManager.class).getDevice(getClientId()); + if (player.reachable()) { + Configuration config = getManager(PlayerManager.class).getConfig(player.id()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, config.serial()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, config.firmwareVersion()); + } + } + + @Override + protected void internalPoll() throws FreeboxException { + super.internalPoll(); + if (thing.getStatus().equals(ThingStatus.ONLINE)) { + Status status = getManager(PlayerManager.class).getPlayerStatus(getClientId()); + updateChannelString(PLAYER_STATUS, PLAYER_STATUS, status.powerState().name()); + ForegroundApp foreground = status.foregroundApp(); + if (foreground != null) { + updateChannelString(PLAYER_STATUS, PACKAGE, foreground._package()); + } + Configuration config = getManager(PlayerManager.class).getConfig(getClientId()); + + uptime = checkUptimeAndFirmware(config.uptimeVal(), uptime, config.firmwareVersion()); + updateChannelQuantity(SYS_INFO, UPTIME, uptime, Units.SECOND); + } + } + + public void reboot() { + processReboot(() -> { + try { + getManager(PlayerManager.class).reboot(getClientId()); + } catch (FreeboxException e) { + logger.warn("Error rebooting: {}", e.getMessage()); + } + }); + } + + @Override + public Collection> getServices() { + return Collections.singletonList(ActivePlayerActions.class); + } + + @Override + public ChannelUID getEventChannelUID() { + return eventChannelUID; + } + + @Override + public void triggerChannel(ChannelUID channelUID, String event) { + super.triggerChannel(channelUID, event); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AirMediaSink.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AirMediaSink.java new file mode 100644 index 00000000000..7d0d4800111 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AirMediaSink.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.core.audio.AudioFormat.*; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Action; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.MediaType; +import org.openhab.core.audio.AudioFormat; +import org.openhab.core.audio.AudioHTTPServer; +import org.openhab.core.audio.AudioSinkAsync; +import org.openhab.core.audio.AudioStream; +import org.openhab.core.audio.StreamServed; +import org.openhab.core.audio.URLAudioStream; +import org.openhab.core.audio.UnsupportedAudioFormatException; +import org.openhab.core.audio.UnsupportedAudioStreamException; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ThingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AirMediaSink} is holding AudioSink capabilities for various + * things. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AirMediaSink extends AudioSinkAsync { + private static final Set> SUPPORTED_STREAMS = Set.of(AudioStream.class); + private static final Set BASIC_FORMATS = Set.of(WAV, OGG); + private static final Set ALL_MP3_FORMATS = Set.of( + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 96000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 112000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 128000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 160000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 192000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 224000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 256000, null), + new AudioFormat(CONTAINER_NONE, CODEC_MP3, null, null, 320000, null)); + + private final Logger logger = LoggerFactory.getLogger(AirMediaSink.class); + private final ApiConsumerHandler thingHandler; + private final Set supportedFormats = new HashSet<>(); + private final AudioHTTPServer audioHTTPServer; + private final String callbackUrl; + private final String playerName; + private final String password; + + public AirMediaSink(ApiConsumerHandler thingHandler, AudioHTTPServer audioHTTPServer, String callbackUrl, + String playerName, String password, boolean acceptAllMp3) { + this.thingHandler = thingHandler; + this.audioHTTPServer = audioHTTPServer; + this.playerName = playerName; + this.callbackUrl = callbackUrl; + this.password = password; + + supportedFormats.addAll(BASIC_FORMATS); + if (acceptAllMp3) { + supportedFormats.addAll(ALL_MP3_FORMATS); + } else { // Only accept MP3 bitrates >= 96 kbps + supportedFormats.add(MP3); + } + } + + @Override + public Set> getSupportedStreams() { + return SUPPORTED_STREAMS; + } + + @Override + public PercentType getVolume() throws IOException { + logger.debug("getVolume received but AirMedia does not have the capability - returning 100%."); + return PercentType.HUNDRED; + } + + @Override + public void setVolume(PercentType volume) throws IOException { + logger.debug("setVolume received but AirMedia does not have the capability - ignoring it."); + } + + @Override + public String getId() { + return thingHandler.getThing().getUID().toString(); + } + + @Override + public @Nullable String getLabel(@Nullable Locale locale) { + return thingHandler.getThing().getLabel(); + } + + @Override + protected void processAsynchronously(@Nullable AudioStream audioStream) + throws UnsupportedAudioFormatException, UnsupportedAudioStreamException { + if (thingHandler.getThing().getStatus() == ThingStatus.ONLINE) { + try { + MediaReceiverManager manager = thingHandler.getManager(MediaReceiverManager.class); + if (audioStream == null) { + manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO); + return; + } + + if (audioStream instanceof URLAudioStream urlAudioStream) { + // it is an external URL, we can access it directly + logger.debug("AirPlay audio sink: process url {}", urlAudioStream.getURL()); + playMedia(manager, urlAudioStream.getURL()); + return; + } + // we serve it on our own HTTP server + StreamServed streamServed; + try { + streamServed = audioHTTPServer.serve(audioStream, 5, true); + } catch (IOException e) { + try { + audioStream.close(); + } catch (IOException ex) { + logger.debug("Exception while closing audioStream"); + } + throw new UnsupportedAudioStreamException( + "AirPlay device was not able to handle the audio stream (cache on disk failed).", + audioStream.getClass(), e); + } + streamServed.playEnd().thenRun(() -> this.playbackFinished(audioStream)); + logger.debug("AirPlay audio sink: process url {}", callbackUrl + streamServed.url()); + playMedia(manager, callbackUrl + streamServed.url()); + } catch (FreeboxException e) { + logger.warn("Audio stream playback failed: {}", e.getMessage()); + } + } + } + + private void playMedia(MediaReceiverManager manager, String url) throws FreeboxException { + manager.sendToReceiver(playerName, password, Action.STOP, MediaType.VIDEO); + manager.sendToReceiver(playerName, password, Action.START, MediaType.VIDEO, url); + } + + @Override + public Set getSupportedFormats() { + return supportedFormats; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AlarmHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AlarmHandler.java new file mode 100644 index 00000000000..ba90fda06ed --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/AlarmHandler.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EndpointState; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link AlarmHandler} is responsible for handling everything associated to + * any Freebox Home Alarm thing type. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class AlarmHandler extends HomeNodeHandler { + + public AlarmHandler(Thing thing) { + super(thing); + } + + @Override + protected State getChannelState(HomeManager homeManager, String channelId, EndpointState state) { + String value = state.value(); + + if (value == null) { + return UnDefType.NULL; + } + + return switch (channelId) { + case NODE_BATTERY -> DecimalType.valueOf(value); + case ALARM_PIN -> StringType.valueOf(value); + case ALARM_SOUND, ALARM_VOLUME -> QuantityType.valueOf(value + " %"); + case ALARM_TIMEOUT1, ALARM_TIMEOUT2, ALARM_TIMEOUT3 -> QuantityType.valueOf(value + " s"); + default -> UnDefType.NULL; + }; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java new file mode 100644 index 00000000000..ed4a699a0d1 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerHandler.java @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.Source; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.MediaType; +import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Receiver; +import org.openhab.binding.freeboxos.internal.api.rest.RestManager; +import org.openhab.binding.freeboxos.internal.config.ApiConsumerConfiguration; +import org.openhab.binding.freeboxos.internal.config.ClientConfiguration; +import org.openhab.core.audio.AudioSink; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import inet.ipaddr.IPAddress; +import inet.ipaddr.MACAddressString; +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link ServerHandler} is a base abstract class for all devices made available by the FreeboxOs bridge + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsumerIntf { + private final Logger logger = LoggerFactory.getLogger(ApiConsumerHandler.class); + private final Map> jobs = new HashMap<>(); + + private @Nullable ServiceRegistration reg; + + ApiConsumerHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + FreeboxOsHandler bridgeHandler = checkBridgeHandler(); + if (bridgeHandler == null) { + return; + } + + Map properties = editProperties(); + if (properties.isEmpty()) { + try { + initializeProperties(properties); + checkAirMediaCapabilities(properties); + updateProperties(properties); + } catch (FreeboxException e) { + logger.warn("Error getting thing {} properties: {}", thing.getUID(), e.getMessage()); + } + } + + boolean isAudioReceiver = Boolean.parseBoolean(properties.get(MediaType.AUDIO.name())); + if (isAudioReceiver) { + configureMediaSink(bridgeHandler, properties.getOrDefault(Source.UPNP.name(), "")); + } + + startRefreshJob(); + } + + private void configureMediaSink(FreeboxOsHandler bridgeHandler, String upnpName) { + try { + Receiver receiver = getManager(MediaReceiverManager.class).getReceiver(upnpName); + if (receiver != null && reg == null) { + ApiConsumerConfiguration config = getConfig().as(ApiConsumerConfiguration.class); + String callbackURL = bridgeHandler.getCallbackURL(); + if (!config.password.isEmpty() || !receiver.passwordProtected()) { + reg = bridgeHandler.getBundleContext().registerService( + AudioSink.class.getName(), new AirMediaSink(this, bridgeHandler.getAudioHTTPServer(), + callbackURL, receiver.name(), config.password, config.acceptAllMp3), + new Hashtable<>()); + } else { + logger.info("A password needs to be configured to enable Air Media capability."); + } + } + } catch (FreeboxException e) { + logger.warn("Unable to retrieve Media Receivers: {}", e.getMessage()); + } + } + + public T getManager(Class clazz) throws FreeboxException { + FreeboxOsHandler handler = checkBridgeHandler(); + if (handler != null) { + return handler.getManager(clazz); + } + throw new FreeboxException("Bridge handler not yet defined"); + } + + abstract void initializeProperties(Map properties) throws FreeboxException; + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + if (checkBridgeHandler() != null) { + startRefreshJob(); + } else { + stopJobs(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType || getThing().getStatus() != ThingStatus.ONLINE) { + return; + } + try { + if (checkBridgeHandler() == null || !internalHandleCommand(channelUID.getIdWithoutGroup(), command)) { + logger.debug("Unexpected command {} on channel {}", command, channelUID.getId()); + } + } catch (FreeboxException e) { + logger.warn("Error handling command: {}", e.getMessage()); + } + } + + private void checkAirMediaCapabilities(Map properties) throws FreeboxException { + String upnpName = properties.getOrDefault(Source.UPNP.name(), ""); + Receiver receiver = getManager(MediaReceiverManager.class).getReceiver(upnpName); + if (receiver != null) { + receiver.capabilities().entrySet() + .forEach(entry -> properties.put(entry.getKey().name(), entry.getValue().toString())); + } + } + + private @Nullable FreeboxOsHandler checkBridgeHandler() { + Bridge bridge = getBridge(); + if (bridge != null) { + BridgeHandler handler = bridge.getHandler(); + if (handler instanceof FreeboxOsHandler) { + if (bridge.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + return (FreeboxOsHandler) handler; + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + } + return null; + } + + @Override + public void dispose() { + stopJobs(); + ServiceRegistration localReg = reg; + if (localReg != null) { + localReg.unregister(); + } + super.dispose(); + } + + private void startRefreshJob() { + removeJob("GlobalJob"); + + int refreshInterval = getConfigAs(ApiConsumerConfiguration.class).refreshInterval; + logger.debug("Scheduling state update every {} seconds for thing {}...", refreshInterval, getThing().getUID()); + + ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail(); + if (ThingStatusDetail.DUTY_CYCLE.equals(detail)) { + try { + internalPoll(); + } catch (FreeboxException ignore) { + // An exception is normal if the box is rebooting then let's try again later... + addJob("Initialize", this::initialize, 10, TimeUnit.SECONDS); + return; + } + } + + addJob("GlobalJob", () -> { + try { + internalPoll(); + } catch (FreeboxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + }, 0, refreshInterval, TimeUnit.SECONDS); + } + + private void removeJob(String name) { + ScheduledFuture existing = jobs.get(name); + if (existing != null && !existing.isCancelled()) { + existing.cancel(true); + } + } + + @Override + public void addJob(String name, Runnable command, long initialDelay, long delay, TimeUnit unit) { + removeJob(name); + jobs.put(name, scheduler.scheduleWithFixedDelay(command, initialDelay, delay, unit)); + } + + @Override + public void addJob(String name, Runnable command, long delay, TimeUnit unit) { + removeJob(name); + jobs.put(name, scheduler.schedule(command, delay, unit)); + } + + @Override + public void stopJobs() { + jobs.keySet().forEach(name -> removeJob(name)); + jobs.clear(); + } + + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + return false; + } + + protected abstract void internalPoll() throws FreeboxException; + + private void updateIfActive(String group, String channelId, State state) { + ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId); + if (isLinked(id)) { + updateState(id, state); + } + } + + protected void updateIfActive(String channelId, State state) { + ChannelUID id = new ChannelUID(getThing().getUID(), channelId); + if (isLinked(id)) { + updateState(id, state); + } + } + + protected void updateChannelDateTimeState(String channelId, @Nullable ZonedDateTime timestamp) { + updateIfActive(channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp)); + } + + protected void updateChannelDateTimeState(String group, String channelId, @Nullable ZonedDateTime timestamp) { + updateIfActive(group, channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp)); + } + + protected void updateChannelOnOff(String group, String channelId, boolean value) { + updateIfActive(group, channelId, OnOffType.from(value)); + } + + protected void updateChannelOnOff(String channelId, boolean value) { + updateIfActive(channelId, OnOffType.from(value)); + } + + protected void updateChannelString(String group, String channelId, @Nullable String value) { + updateIfActive(group, channelId, value != null ? new StringType(value) : UnDefType.NULL); + } + + protected void updateChannelString(String group, String channelId, @Nullable IPAddress value) { + updateIfActive(group, channelId, value != null ? new StringType(value.toCanonicalString()) : UnDefType.NULL); + } + + protected void updateChannelString(String channelId, @Nullable String value) { + updateIfActive(channelId, value != null ? new StringType(value) : UnDefType.NULL); + } + + protected void updateChannelString(String channelId, Enum value) { + updateIfActive(channelId, new StringType(value.name())); + } + + protected void updateChannelString(String group, String channelId, Enum value) { + updateIfActive(group, channelId, new StringType(value.name())); + } + + protected void updateChannelQuantity(String group, String channelId, double d, Unit unit) { + updateChannelQuantity(group, channelId, new QuantityType<>(d, unit)); + } + + protected void updateChannelQuantity(String channelId, @Nullable QuantityType quantity) { + updateIfActive(channelId, quantity != null ? quantity : UnDefType.NULL); + } + + protected void updateChannelQuantity(String group, String channelId, @Nullable QuantityType quantity) { + updateIfActive(group, channelId, quantity != null ? quantity : UnDefType.NULL); + } + + protected void updateChannelDecimal(String group, String channelId, @Nullable Integer value) { + updateIfActive(group, channelId, value != null ? new DecimalType(value) : UnDefType.NULL); + } + + protected void updateChannelQuantity(String group, String channelId, QuantityType qtty, Unit unit) { + updateChannelQuantity(group, channelId, qtty.toUnit(unit)); + } + + @Override + public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + super.updateStatus(status, statusDetail, description); + } + + @Override + public Map editProperties() { + return super.editProperties(); + } + + @Override + public void updateProperties(@Nullable Map properties) { + super.updateProperties(properties); + } + + @Override + public Configuration getConfig() { + return super.getConfig(); + } + + @Override + public int getClientId() { + return ((BigDecimal) getConfig().get(ClientConfiguration.ID)).intValue(); + } + + @Override + public MACAddress getMac() { + String mac = (String) getConfig().get(Thing.PROPERTY_MAC_ADDRESS); + return new MACAddressString(mac).getAddress(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerIntf.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerIntf.java new file mode 100644 index 00000000000..d1c43caa4ee --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ApiConsumerIntf.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.config.ClientConfiguration; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.ThingHandler; + +import inet.ipaddr.MACAddressString; +import inet.ipaddr.mac.MACAddress; + +/** + * The {@link ApiConsumerIntf} defines some common methods for various devices (server, player, repeater) not belonging + * to the same class hierarchy + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public interface ApiConsumerIntf extends ThingHandler { + + Map editProperties(); + + Configuration getConfig(); + + void updateProperties(@Nullable Map properties); + + void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description); + + void stopJobs(); + + void addJob(String name, Runnable command, long initialDelay, long delay, TimeUnit unit); + + void addJob(String name, Runnable command, long delay, TimeUnit unit); + + default int getClientId() { + return ((BigDecimal) getConfig().get(ClientConfiguration.ID)).intValue(); + } + + default MACAddress getMac() { + String mac = (String) getConfig().get(Thing.PROPERTY_MAC_ADDRESS); + return new MACAddressString(mac).getAddress(); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/BasicShutterHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/BasicShutterHandler.java new file mode 100644 index 00000000000..4a51d8bf883 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/BasicShutterHandler.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Endpoint; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EndpointState; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link BasicShutterHandler} is responsible for handling everything associated to + * any Freebox Home basic-shutter thing type. + * + * @author ben12 - Initial contribution + */ +@NonNullByDefault +public class BasicShutterHandler extends HomeNodeHandler { + private static final Set SHUTTER_ENDPOINTS = Set.of(SHUTTER_STOP, BASIC_SHUTTER_UP, BASIC_SHUTTER_DOWN); + + public BasicShutterHandler(Thing thing) { + super(thing); + } + + @Override + protected void internalConfigureChannel(String channelId, Configuration conf, List endpoints) { + endpoints.stream().filter(ep -> channelId.equals(BASIC_SHUTTER_STATE) && SHUTTER_ENDPOINTS.contains(ep.name())) + .forEach(endPoint -> conf.put(endPoint.name(), endPoint.id())); + } + + @Override + protected State getChannelState(HomeManager homeManager, String channelId, EndpointState state) { + String value = state.value(); + return value != null && channelId.equals(BASIC_SHUTTER_STATE) + ? state.asBoolean() ? OpenClosedType.CLOSED : OpenClosedType.OPEN + : UnDefType.NULL; + } + + @Override + protected boolean executeChannelCommand(HomeManager homeManager, String channelId, Command command, + Configuration config) throws FreeboxException { + Integer slot = getSlotId(config, command.toString().toLowerCase()); + if (BASIC_SHUTTER_STATE.equals(channelId) && slot != null) { + return homeManager.putCommand(getClientId(), slot, true); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CallHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CallHandler.java new file mode 100644 index 00000000000..780f9fab79e --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CallHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.CallActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.CallManager; +import org.openhab.binding.freeboxos.internal.api.rest.CallManager.Call; +import org.openhab.binding.freeboxos.internal.api.rest.CallManager.Type; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link CallHandler} is responsible for handling everything associated to the phone calls received on the box line + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class CallHandler extends ApiConsumerHandler { + private final Logger logger = LoggerFactory.getLogger(CallHandler.class); + private final Map lastCalls = new HashMap<>(Type.values().length); + + public CallHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + // nothing to do here + } + + @Override + protected void internalPoll() throws FreeboxException { + logger.debug("Polling phone calls ..."); + + lastCalls.clear(); + + List entries = getManager(CallManager.class).getCallEntries(); + Arrays.stream(Type.values()).forEach(callType -> entries.stream().filter(call -> call.type().equals(callType)) + .reduce((first, second) -> second).ifPresent(this::updateCallChannels)); + + // Clear incoming call if the youngest is not an incoming call + lastCalls.entrySet().stream().sorted(Map.Entry.comparingByValue()).reduce((first, second) -> second) + .map(entry -> entry.getKey()).filter(type -> !Type.INCOMING.equals(type)).ifPresent(type -> { + String groupName = Type.INCOMING.name().toLowerCase(); + getThing().getChannelsOfGroup(groupName).stream().map(Channel::getUID).filter(uid -> isLinked(uid)) + .forEach(uid -> updateState(uid, UnDefType.NULL)); + }); + + updateStatus(ThingStatus.ONLINE); + } + + private void updateCallChannels(Call call) { + Type lastType = call.type(); + lastCalls.put(lastType, call.datetime()); + String group = lastType.name().toLowerCase(); + String phoneNumber = call.number(); + + updateChannelString(group, NUMBER, phoneNumber); + updateChannelString(group, NAME, call.name()); + updateChannelDateTimeState(group, TIMESTAMP, call.datetime()); + + // Do not consider duration for Missed & incoming calls + if (lastType == Type.ACCEPTED || lastType == Type.OUTGOING) { + updateChannelQuantity(group, DURATION, call.duration(), Units.SECOND); + } + } + + public void emptyQueue() { + try { + getManager(CallManager.class).emptyQueue(); + } catch (FreeboxException e) { + logger.warn("Error clearing call logs: {}", e.getMessage()); + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(CallActions.class); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CameraHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CameraHandler.java new file mode 100644 index 00000000000..e237317b87c --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/CameraHandler.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link CameraHandler} is responsible for handling everything associated to + * any Freebox Home Camera thing type. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class CameraHandler extends ApiConsumerHandler { + + public CameraHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + } + + @Override + protected void internalPoll() throws FreeboxException { + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + return super.internalHandleCommand(channelId, command); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/DectHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/DectHandler.java new file mode 100644 index 00000000000..ae89dc28df0 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/DectHandler.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Config; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Status; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * The {@link DectHandler} is responsible for handling DECT specifics of the Telephony API + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class DectHandler extends FxsHandler { + + public DectHandler(Thing thing) { + super(thing); + } + + @Override + protected void updateConfigChannels(Config config) { + super.updateConfigChannels(config); + updateChannelOnOff(DECT_ACTIVE, config.dectEnabled()); + updateChannelOnOff(ALTERNATE_RING, config.dectRingOnOff()); + } + + @Override + protected void updateStatusChannels(Status status) { + super.updateStatusChannels(status); + updateIfActive(GAIN_RX, new PercentType(status.gainRx())); + updateIfActive(GAIN_TX, new PercentType(status.gainTx())); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + PhoneManager phoneManager = getManager(PhoneManager.class); + if (command instanceof OnOffType) { + boolean status = OnOffType.ON.equals(command); + if (RINGING.equals(channelId)) { + phoneManager.ringDect(status); + return true; + } else if (DECT_ACTIVE.equals(channelId)) { + phoneManager.setStatus(status); + return true; + } else if (ALTERNATE_RING.equals(channelId)) { + phoneManager.alternateRing(status); + return true; + } + } + if (command instanceof PercentType) { + PercentType percent = (PercentType) command; + if (GAIN_RX.equals(channelId)) { + phoneManager.setGainRx(getClientId(), percent.intValue()); + updateIfActive(GAIN_RX, percent); + return true; + } else if (GAIN_TX.equals(channelId)) { + phoneManager.setGainTx(getClientId(), percent.intValue()); + updateIfActive(GAIN_RX, percent); + return true; + } + } + return super.internalHandleCommand(channelId, command); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeDeviceIntf.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeDeviceIntf.java new file mode 100644 index 00000000000..20dc1345f2b --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeDeviceIntf.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; + +/** + * The {@link FreeDeviceIntf} defines some common methods for various devices (server, player, repeater) not belonging + * to the same class hierarchy + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public interface FreeDeviceIntf extends ApiConsumerIntf { + public ChannelUID getEventChannelUID(); + + public void triggerChannel(ChannelUID channelUID, String event); + + default long checkUptimeAndFirmware(long newUptime, long oldUptime, String firmwareVersion) { + if (newUptime < oldUptime) { + triggerChannel(getEventChannelUID(), "restarted"); + Map properties = editProperties(); + if (!firmwareVersion.equals(properties.get(Thing.PROPERTY_FIRMWARE_VERSION))) { + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion); + updateProperties(properties); + triggerChannel(getEventChannelUID(), "firmware_updated"); + } + } + return newUptime; + } + + default void processReboot(Runnable actualReboot) { + triggerChannel(getEventChannelUID(), "reboot_requested"); + actualReboot.run(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE, "System rebooting..."); + stopJobs(); + addJob("Initialize", this::initialize, 30, TimeUnit.SECONDS); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeboxOsHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeboxOsHandler.java new file mode 100644 index 00000000000..bb62c4f37b9 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeboxOsHandler.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.FreeboxOsSession; +import org.openhab.binding.freeboxos.internal.api.rest.RestManager; +import org.openhab.binding.freeboxos.internal.config.FreeboxOsConfiguration; +import org.openhab.binding.freeboxos.internal.discovery.FreeboxOsDiscoveryService; +import org.openhab.core.audio.AudioHTTPServer; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FreeboxOsHandler} handle common parts of Freebox bridges. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeboxOsHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(FreeboxOsHandler.class); + private final FreeboxOsSession session; + private final String callbackURL; + private final BundleContext bundleContext; + private final AudioHTTPServer audioHTTPServer; + + private Optional> openConnectionJob = Optional.empty(); + private Optional> grantingJob = Optional.empty(); + + public FreeboxOsHandler(Bridge thing, FreeboxOsSession session, String callbackURL, BundleContext bundleContext, + AudioHTTPServer audioHTTPServer) { + super(thing); + this.session = session; + this.callbackURL = callbackURL; + this.bundleContext = bundleContext; + this.audioHTTPServer = audioHTTPServer; + } + + @Override + public void initialize() { + freeConnectionJob(); + + FreeboxOsConfiguration config = getConfiguration(); + openConnectionJob = Optional.of(scheduler.submit(() -> { + try { + session.initialize(config); + if (config.appToken.isBlank()) { + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "@text/info-conf-pending"); + grantingJob = Optional.of(scheduler.schedule(this::processGranting, 2, TimeUnit.SECONDS)); + return; + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE); + session.openSession(config.appToken); + } + updateStatus(ThingStatus.ONLINE); + } catch (FreeboxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + })); + } + + private void processGranting() { + try { + String appToken = session.grant(); + if (appToken.isBlank()) { + grantingJob = Optional.of(scheduler.schedule(this::processGranting, 2, TimeUnit.SECONDS)); + } else { + Configuration thingConfig = editConfiguration(); + thingConfig.put(FreeboxOsConfiguration.APP_TOKEN, appToken); + updateConfiguration(thingConfig); + logger.info("AppToken updated, ensure giving permissions in the Freebox management console"); + initialize(); + } + } catch (FreeboxException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + public T getManager(Class clazz) throws FreeboxException { + return session.getManager(clazz); + } + + private void freeConnectionJob() { + openConnectionJob.ifPresent(job -> job.cancel(true)); + openConnectionJob = Optional.empty(); + grantingJob.ifPresent(job -> job.cancel(true)); + grantingJob = Optional.empty(); + } + + @Override + public void dispose() { + freeConnectionJob(); + session.closeSession(); + + super.dispose(); + } + + @Override + public Collection> getServices() { + return Collections.singleton(FreeboxOsDiscoveryService.class); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + public FreeboxOsConfiguration getConfiguration() { + return getConfigAs(FreeboxOsConfiguration.class); + } + + public String getCallbackURL() { + return callbackURL; + } + + public BundleContext getBundleContext() { + return bundleContext; + } + + public AudioHTTPServer getAudioHTTPServer() { + return audioHTTPServer; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeplugHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeplugHandler.java new file mode 100644 index 00000000000..9849ae5f42f --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FreeplugHandler.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.FreeplugActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.FreeplugManager; +import org.openhab.binding.freeboxos.internal.api.rest.FreeplugManager.NetRole; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FreeplugHandler} is responsible for handling everything associated to a CPL gateway managed by the freebox + * server + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FreeplugHandler extends ApiConsumerHandler { + private final Logger logger = LoggerFactory.getLogger(FreeplugHandler.class); + + public FreeplugHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + getManager(FreeplugManager.class).getPlug(getMac()).ifPresent(plug -> { + NetRole role = plug.netRole(); + properties.put(Thing.PROPERTY_MODEL_ID, plug.model()); + properties.put(ROLE, role.name()); + properties.put(NET_ID, plug.netId()); + properties.put(ETHERNET_SPEED, String.format("%d Mb/s", plug.ethSpeed())); + properties.put(LOCAL, Boolean.valueOf(plug.local()).toString()); + properties.put(FULL_DUPLEX, Boolean.valueOf(plug.ethFullDuplex()).toString()); + + if (role.equals(NetRole.CCO)) { // Coordinator does not provide rate up or down + List channels = new ArrayList<>(getThing().getChannels()); + channels.removeIf(channel -> channel.getUID().getId().contains("rate")); + updateThing(editThing().withChannels(channels).build()); + } + }); + } + + @Override + protected void internalPoll() throws FreeboxException { + getManager(FreeplugManager.class).getPlug(getMac()).ifPresent(plug -> { + ZonedDateTime lastSeen = ZonedDateTime.now().minusSeconds(plug.inactive()); + updateChannelDateTimeState(LAST_SEEN, lastSeen); + + updateChannelString(LINE_STATUS, plug.ethPortStatus()); + updateChannelOnOff(REACHABLE, plug.hasNetwork()); + + updateRateChannel(RATE + "-down", plug.rxRate()); + updateRateChannel(RATE + "-up", plug.txRate()); + }); + } + + private void updateRateChannel(String channel, int rate) { + QuantityType qtty = rate != -1 ? new QuantityType<>(rate, Units.MEGABIT_PER_SECOND) : null; + updateChannelQuantity(channel, qtty); + } + + public void reset() { + try { + getManager(FreeplugManager.class).reboot(getMac()); + logger.debug("Freeplug {} succesfully restarted", getMac()); + } catch (FreeboxException e) { + logger.warn("Error restarting freeplug: {}", e.getMessage()); + } + } + + @Override + public Collection> getServices() { + return Collections.singleton(FreeplugActions.class); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FxsHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FxsHandler.java new file mode 100644 index 00000000000..2486c3be624 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/FxsHandler.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Config; +import org.openhab.binding.freeboxos.internal.api.rest.PhoneManager.Status; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FxsHandler} is responsible for handling everything associated to the landline associated with the + * Freebox Server. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class FxsHandler extends ApiConsumerHandler { + private final Logger logger = LoggerFactory.getLogger(FxsHandler.class); + + public FxsHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + getManager(PhoneManager.class).getStatus(getClientId()) + .ifPresent(status -> properties.put(Thing.PROPERTY_VENDOR, status.vendor())); + } + + @Override + protected void internalPoll() throws FreeboxException { + logger.debug("Polling landline status..."); + + Config config = getManager(PhoneManager.class).getConfig(); + updateConfigChannels(config); + + getManager(PhoneManager.class).getStatus(getClientId()).ifPresent(this::updateStatusChannels); + } + + protected void updateConfigChannels(Config config) { + updateChannelString(TELEPHONY_SERVICE, config.network()); + } + + protected void updateStatusChannels(Status status) { + updateChannelOnOff(ONHOOK, status.onHook()); + updateChannelOnOff(RINGING, status.isRinging()); + updateChannelString(HARDWARE_STATUS, status.hardwareDefect() ? "KO" : "OK"); + updateStatus(ThingStatus.ONLINE); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + if (RINGING.equals(channelId) && command instanceof OnOffType) { + getManager(PhoneManager.class).ringFxs(TRUE_COMMANDS.contains(command)); + return true; + } + return super.internalHandleCommand(channelId, command); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HomeNodeHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HomeNodeHandler.java new file mode 100644 index 00000000000..33c8f12360e --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HomeNodeHandler.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Endpoint; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EndpointState; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EpType; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.HomeNode; +import org.openhab.binding.freeboxos.internal.config.ApiConsumerConfiguration; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +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; + +/** + * The {@link HomeNodeHandler} is the base class for handler of home node things. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public abstract class HomeNodeHandler extends ApiConsumerHandler { + private final Logger logger = LoggerFactory.getLogger(HomeNodeHandler.class); + + public HomeNodeHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + HomeNode node = getManager(HomeManager.class).getHomeNode(getClientId()); + + // Gets the lowest refresh time or else, we'll keep configuration default + node.showEndpoints().stream().filter(ep -> ep.epType() == EpType.SIGNAL).filter(ep -> ep.refresh() != 0) + .min(Comparator.comparing(Endpoint::refresh)).map(Endpoint::refresh).ifPresent(rate -> { + Configuration thingConfig = editConfiguration(); + thingConfig.put(ApiConsumerConfiguration.REFRESH_INTERVAL, Integer.toString(rate / 1000)); + updateConfiguration(thingConfig); + }); + + properties.putAll(node.props()); + + getThing().getChannels().forEach(channel -> { + Configuration conf = channel.getConfiguration(); + node.type().endpoints().stream().filter(ep -> ep.name().equals(channel.getUID().getIdWithoutGroup())) + .forEach(endPoint -> conf.put(endPoint.epType().asConfId(), endPoint.id())); + internalConfigureChannel(channel.getUID().getIdWithoutGroup(), conf, node.type().endpoints()); + }); + } + + protected void internalConfigureChannel(String channelId, Configuration conf, List endpoints) { + } + + @Override + protected void internalPoll() throws FreeboxException { + HomeManager homeManager = getManager(HomeManager.class); + getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID())).forEach(channel -> { + State result = UnDefType.UNDEF; + Integer slotId = getSlotId(channel.getConfiguration(), EpType.SIGNAL.asConfId()); + if (slotId instanceof Integer) { + try { + EndpointState state = homeManager.getEndpointsState(getClientId(), slotId); + if (state != null) { + result = getChannelState(homeManager, channel.getUID().getIdWithoutGroup(), state); + } else { + result = getChannelState(homeManager, channel.getUID().getIdWithoutGroup()); + } + } catch (FreeboxException e) { + logger.warn("Error updating channel: {}", e.getMessage()); + } + } else { + result = getChannelState(homeManager, channel.getUID().getIdWithoutGroup()); + } + updateState(channel.getUID(), result); + }); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + Channel channel = getThing().getChannel(channelId); + if (channel != null) { + Configuration config = channel.getConfiguration(); + String channelWG = channel.getUID().getIdWithoutGroup(); + Integer slotId = getSlotId(config, EpType.SLOT.asConfId()); + HomeManager homeManager = getManager(HomeManager.class); + return slotId instanceof Integer ? executeSlotCommand(homeManager, channelWG, command, config, slotId) + : executeChannelCommand(homeManager, channelWG, command, config); + } + return super.internalHandleCommand(channelId, command); + } + + protected @Nullable Integer getSlotId(Configuration configuration, String endPoint) { + Object slot = configuration.get(endPoint); + return slot instanceof BigDecimal slotId ? slotId.intValue() : null; + } + + protected boolean executeChannelCommand(HomeManager homeManager, String channelId, Command command, + Configuration config) throws FreeboxException { + return false; + } + + protected boolean executeSlotCommand(HomeManager homeManager, String channelId, Command command, + Configuration config, int slotId) throws FreeboxException { + return false; + } + + protected State getChannelState(HomeManager homeManager, String channelWG) { + return UnDefType.UNDEF; + } + + protected abstract State getChannelState(HomeManager homeManager, String channelId, EndpointState state); +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java new file mode 100644 index 00000000000..d7966549eca --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/HostHandler.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.HostActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.HostIntf; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.Source; +import org.openhab.binding.freeboxos.internal.api.rest.WebSocketManager; +import org.openhab.binding.freeboxos.internal.config.ApiConsumerConfiguration; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HostHandler} is responsible for all network equipments hosted on the network + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class HostHandler extends ApiConsumerHandler { + private final Logger logger = LoggerFactory.getLogger(HostHandler.class); + + // We start in pull mode and switch to push after a first update + private boolean pushSubscribed = false; + + public HostHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + getManager(LanBrowserManager.class).getHost(getMac()).ifPresent(result -> { + LanHost host = result.host(); + properties.put(Thing.PROPERTY_VENDOR, host.vendorName()); + host.getUPnPName().ifPresent(upnpName -> properties.put(Source.UPNP.name(), upnpName)); + }); + } + + @Override + public void dispose() { + try { + getManager(WebSocketManager.class).unregisterListener(getMac()); + } catch (FreeboxException e) { + logger.warn("Error unregistering host from the websocket: {}", e.getMessage()); + } + super.dispose(); + } + + @Override + protected void internalPoll() throws FreeboxException { + if (pushSubscribed) { + return; + } + HostIntf data = getManager(LanBrowserManager.class).getHost(getMac()) + .orElseThrow(() -> new FreeboxException("Host data not found")); + + updateConnectivityChannels(data.host()); + logger.debug("Switching to push mode - refreshInterval will now be ignored for Connectivity data"); + getManager(WebSocketManager.class).registerListener(data.host().getMac(), this); + pushSubscribed = true; + } + + public void updateConnectivityChannels(LanHost host) { + updateChannelOnOff(CONNECTIVITY, REACHABLE, host.reachable()); + updateChannelDateTimeState(CONNECTIVITY, LAST_SEEN, host.getLastSeen()); + updateChannelString(CONNECTIVITY, IP_ADDRESS, host.getIpv4()); + updateStatus(host.reachable() ? ThingStatus.ONLINE : ThingStatus.OFFLINE); + } + + public void wol() { + try { + getManager(LanBrowserManager.class).wakeOnLan(getMac(), + getConfigAs(ApiConsumerConfiguration.class).password); + } catch (FreeboxException e) { + logger.warn("Error waking up host: {}", e.getMessage()); + } + } + + @Override + public Collection> getServices() { + return Collections.singletonList(HostActions.class); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/KeyfobHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/KeyfobHandler.java new file mode 100644 index 00000000000..96c601f3262 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/KeyfobHandler.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EndpointState; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link KeyfobHandler} is responsible for handling everything associated to + * any Freebox Home keyfob thing type. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class KeyfobHandler extends HomeNodeHandler { + + public KeyfobHandler(Thing thing) { + super(thing); + } + + @Override + protected State getChannelState(HomeManager homeManager, String channelId, EndpointState state) { + String value = state.value(); + if (value != null) { + switch (channelId) { + case KEYFOB_ENABLE: + return OnOffType.from(state.asBoolean()); + case NODE_BATTERY: + return DecimalType.valueOf(value); + } + } + return UnDefType.NULL; + } + + @Override + protected boolean executeSlotCommand(HomeManager homeManager, String channelId, Command command, + Configuration config, int intValue) throws FreeboxException { + if (KEYFOB_ENABLE.equals(channelId) && command instanceof OnOffType onOff) { + return getManager(HomeManager.class).putCommand(getClientId(), intValue, onOff); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/PlayerHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/PlayerHandler.java new file mode 100644 index 00000000000..c4eb9594735 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/PlayerHandler.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.KEY_CODE; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.action.PlayerActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager; +import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.Player; +import org.openhab.binding.freeboxos.internal.config.PlayerConfiguration; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import inet.ipaddr.IPAddress; + +/** + * The {@link PlayerHandler} is responsible for handling everything associated to any Freebox Player thing type. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class PlayerHandler extends HostHandler { + private static final List VALID_REMOTE_KEYS = Arrays.asList("red", "green", "blue", "yellow", "power", + "list", "tv", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "vol_inc", "vol_dec", "mute", "prgm_inc", + "prgm_dec", "prev", "bwd", "play", "rec", "fwd", "next", "up", "right", "down", "left", "back", "swap", + "info", "epg", "mail", "media", "help", "options", "pip", "ok", "home"); + + private final Logger logger = LoggerFactory.getLogger(PlayerHandler.class); + private @Nullable IPAddress ipAddress; + + public PlayerHandler(Thing thing) { + super(thing); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + super.initializeProperties(properties); + Player player = getManager(PlayerManager.class).getDevice(getClientId()); + properties.put(Thing.PROPERTY_MODEL_ID, player.deviceModel().name()); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + if (KEY_CODE.equals(channelId) && command instanceof StringType) { + sendKey(command.toString(), false, 1); + return true; + } + + return super.internalHandleCommand(channelId, command); + } + + @Override + public void updateConnectivityChannels(LanHost host) { + super.updateConnectivityChannels(host); + ipAddress = host.getIpv4(); + } + + public void sendKey(String key, boolean longPress, int count) { + String aKey = key.toLowerCase(); + IPAddress ip = ipAddress; + if (ip == null) { + logger.warn("Player IP is unknown"); + } else if (VALID_REMOTE_KEYS.contains(aKey)) { + String remoteCode = (String) getConfig().get(PlayerConfiguration.REMOTE_CODE); + if (remoteCode != null) { + try { + getManager(PlayerManager.class).sendKey(ip.toCanonicalString(), remoteCode, aKey, longPress, count); + } catch (FreeboxException e) { + logger.warn("Error sending key: {}", e.getMessage()); + } + } else { + logger.warn("A remote code must be configured in the on the player thing."); + } + } else { + logger.warn("Key '{}' is not a valid key expression", key); + } + } + + public void sendMultipleKeys(String keys) { + String[] keyChain = keys.split(","); + Arrays.stream(keyChain).forEach(key -> { + sendKey(key, false, 1); + }); + } + + @Override + public Collection> getServices() { + return Collections.singletonList(PlayerActions.class); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RepeaterHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RepeaterHandler.java new file mode 100644 index 00000000000..6b345a88cb0 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RepeaterHandler.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.RepeaterActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager; +import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager.Repeater; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RepeaterHandler} is responsible for interface to a freebox + * pop repeater. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class RepeaterHandler extends HostHandler implements FreeDeviceIntf { + private final Logger logger = LoggerFactory.getLogger(RepeaterHandler.class); + private long uptime = -1; + private final ChannelUID eventChannelUID; + + public RepeaterHandler(Thing thing) { + super(thing); + eventChannelUID = new ChannelUID(getThing().getUID(), REPEATER_MISC, BOX_EVENT); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + super.initializeProperties(properties); + + Repeater repeater = getManager(RepeaterManager.class).getDevice(getClientId()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, repeater.sn()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, repeater.firmwareVersion()); + properties.put(Thing.PROPERTY_MODEL_ID, repeater.model().name()); + } + + @Override + protected void internalPoll() throws FreeboxException { + super.internalPoll(); + + if (!thing.getStatus().equals(ThingStatus.ONLINE)) { + return; + } + + logger.debug("Polling Repeater status"); + RepeaterManager repeaterManager = getManager(RepeaterManager.class); + + Repeater repeater = repeaterManager.getDevice(getClientId()); + updateChannelOnOff(REPEATER_MISC, LED, repeater.ledActivated()); + updateChannelString(REPEATER_MISC, CONNECTION_STATUS, repeater.connection()); + + List hosts = repeaterManager.getRepeaterHosts(getClientId()); + updateChannelDecimal(REPEATER_MISC, HOST_COUNT, hosts.size()); + + uptime = checkUptimeAndFirmware(repeater.getUptimeVal(), uptime, repeater.firmwareVersion()); + updateChannelQuantity(REPEATER_MISC, UPTIME, uptime, Units.SECOND); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + if (ON_OFF_CLASSES.contains(command.getClass()) && LED.equals(channelId)) { + getManager(RepeaterManager.class).led(getClientId(), TRUE_COMMANDS.contains(command)) + .ifPresent(repeater -> updateChannelOnOff(REPEATER_MISC, LED, repeater.ledActivated())); + } + return super.internalHandleCommand(channelId, command); + } + + public void reboot() { + processReboot(() -> { + try { + getManager(RepeaterManager.class).reboot(getClientId()); + } catch (FreeboxException e) { + logger.warn("Error rebooting: {}", e.getMessage()); + } + }); + } + + @Override + public Collection> getServices() { + return Collections.singleton(RepeaterActions.class); + } + + @Override + public ChannelUID getEventChannelUID() { + return eventChannelUID; + } + + @Override + public void triggerChannel(ChannelUID channelUID, String event) { + super.triggerChannel(channelUID, event); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RevolutionHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RevolutionHandler.java new file mode 100644 index 00000000000..1771690febe --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/RevolutionHandler.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; +import static org.openhab.core.library.unit.Units.PERCENT; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.LcdManager; +import org.openhab.binding.freeboxos.internal.api.rest.LcdManager.Config; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RevolutionHandler} is responsible for handling take care of revolution server specifics + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class RevolutionHandler extends ServerHandler { + private final Logger logger = LoggerFactory.getLogger(RevolutionHandler.class); + + public RevolutionHandler(Thing thing) { + super(thing); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + LcdManager manager = getManager(LcdManager.class); + Config config = manager.getConfig(); + switch (channelId) { + case LCD_BRIGHTNESS: + setBrightness(manager, config, command); + internalPoll(); + return true; + case LCD_ORIENTATION: + setOrientation(manager, config, command); + internalPoll(); + return true; + case LCD_FORCED: + setForced(manager, config, command); + internalPoll(); + return true; + } + return super.internalHandleCommand(channelId, command); + } + + @Override + protected void internalPoll() throws FreeboxException { + super.internalPoll(); + Config config = getManager(LcdManager.class).getConfig(); + updateChannelQuantity(DISPLAY, LCD_BRIGHTNESS, config.brightness(), PERCENT); + updateChannelDecimal(DISPLAY, LCD_ORIENTATION, config.orientation()); + updateChannelOnOff(DISPLAY, LCD_FORCED, config.orientationForced()); + } + + private void setOrientation(LcdManager manager, Config config, Command command) throws FreeboxException { + if (command instanceof DecimalType) { + manager.setOrientation(((DecimalType) command).intValue()); + } else { + logger.warn("Invalid command {} from channel {}", command, LCD_ORIENTATION); + } + } + + private void setForced(LcdManager manager, Config config, Command command) throws FreeboxException { + if (ON_OFF_CLASSES.contains(command.getClass())) { + manager.setOrientationForced(TRUE_COMMANDS.contains(command)); + } else { + logger.warn("Invalid command {} from channel {}", command, LCD_FORCED); + } + } + + private void setBrightness(LcdManager manager, Config config, Command command) throws FreeboxException { + if (command instanceof IncreaseDecreaseType) { + manager.setBrightness(() -> config.brightness() + (command == IncreaseDecreaseType.INCREASE ? 1 : -1)); + } else if (command instanceof OnOffType) { + manager.setBrightness(() -> command == OnOffType.ON ? 100 : 0); + } else if (command instanceof QuantityType) { + manager.setBrightness(() -> ((QuantityType) command).intValue()); + } else if (command instanceof DecimalType || command instanceof PercentType) { + manager.setBrightness(() -> ((DecimalType) command).intValue()); + } else { + logger.warn("Invalid command {} from channel {}", command, LCD_BRIGHTNESS); + } + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ServerHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ServerHandler.java new file mode 100644 index 00000000000..f6d607efe28 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ServerHandler.java @@ -0,0 +1,215 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; +import static org.openhab.core.library.unit.Units.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.action.ServerActions; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.AfpManager; +import org.openhab.binding.freeboxos.internal.api.rest.AirMediaManager; +import org.openhab.binding.freeboxos.internal.api.rest.ConnectionManager; +import org.openhab.binding.freeboxos.internal.api.rest.ConnectionManager.Status; +import org.openhab.binding.freeboxos.internal.api.rest.FtpManager; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.Source; +import org.openhab.binding.freeboxos.internal.api.rest.LanManager; +import org.openhab.binding.freeboxos.internal.api.rest.LanManager.LanConfig; +import org.openhab.binding.freeboxos.internal.api.rest.SambaManager; +import org.openhab.binding.freeboxos.internal.api.rest.SambaManager.Samba; +import org.openhab.binding.freeboxos.internal.api.rest.SystemManager; +import org.openhab.binding.freeboxos.internal.api.rest.SystemManager.Config; +import org.openhab.binding.freeboxos.internal.api.rest.UPnPAVManager; +import org.openhab.binding.freeboxos.internal.api.rest.WifiManager; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ServerHandler} handle common parts of Freebox bridges. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ServerHandler extends ApiConsumerHandler implements FreeDeviceIntf { + private static final BigDecimal HUNDRED = BigDecimal.valueOf(100); + + private final Logger logger = LoggerFactory.getLogger(ServerHandler.class); + private final ChannelUID eventChannelUID; + + private long uptime = -1; + + public ServerHandler(Thing thing) { + super(thing); + eventChannelUID = new ChannelUID(getThing().getUID(), SYS_INFO, BOX_EVENT); + } + + @Override + void initializeProperties(Map properties) throws FreeboxException { + LanConfig lanConfig = getManager(LanManager.class).getConfig(); + Config config = getManager(SystemManager.class).getConfig(); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, config.serial()); + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, config.firmwareVersion()); + properties.put(Thing.PROPERTY_HARDWARE_VERSION, config.modelInfo().prettyName()); + properties.put(Source.UPNP.name(), lanConfig.name()); + + List channels = new ArrayList<>(getThing().getChannels()); + config.sensors().forEach(sensor -> { + ChannelUID sensorId = new ChannelUID(thing.getUID(), GROUP_SENSORS, sensor.id()); + channels.add( + ChannelBuilder.create(sensorId).withLabel(sensor.name()).withAcceptedItemType("Number:Temperature") + .withType(new ChannelTypeUID(BINDING_ID + ":temperature")).build()); + }); + config.fans().forEach(sensor -> { + ChannelUID sensorId = new ChannelUID(thing.getUID(), GROUP_FANS, sensor.id()); + channels.add(ChannelBuilder.create(sensorId).withLabel(sensor.name()) + .withAcceptedItemType(CoreItemFactory.NUMBER).withType(new ChannelTypeUID(BINDING_ID + ":fanspeed")) + .build()); + }); + updateThing(editThing().withChannels(channels).build()); + } + + @Override + protected void internalPoll() throws FreeboxException { + logger.debug("Polling server state..."); + fetchConnectionStatus(); + fetchSystemConfig(); + + updateChannelOnOff(ACTIONS, WIFI_STATUS, getManager(WifiManager.class).getStatus()); + updateChannelOnOff(ACTIONS, AIRMEDIA_STATUS, getManager(AirMediaManager.class).getStatus()); + updateChannelOnOff(ACTIONS, UPNPAV_STATUS, getManager(UPnPAVManager.class).getStatus()); + + Samba response = getManager(SambaManager.class).getConfig(); + updateChannelOnOff(FILE_SHARING, SAMBA_FILE_STATUS, response.fileShareEnabled()); + updateChannelOnOff(FILE_SHARING, SAMBA_PRINTER_STATUS, response.printShareEnabled()); + updateChannelOnOff(FILE_SHARING, FTP_STATUS, getManager(FtpManager.class).getStatus()); + updateChannelOnOff(FILE_SHARING, AFP_FILE_STATUS, getManager(AfpManager.class).getStatus()); + } + + private void fetchSystemConfig() throws FreeboxException { + Config config = getManager(SystemManager.class).getConfig(); + + config.sensors().forEach(s -> updateChannelQuantity(GROUP_SENSORS, s.id(), s.value(), SIUnits.CELSIUS)); + config.fans().forEach(f -> updateChannelQuantity(GROUP_FANS, f.id(), f.value(), Units.RPM)); + + uptime = checkUptimeAndFirmware(config.uptimeVal(), uptime, config.firmwareVersion()); + updateChannelQuantity(SYS_INFO, UPTIME, uptime, Units.SECOND); + + LanConfig lanConfig = getManager(LanManager.class).getConfig(); + updateChannelString(SYS_INFO, IP_ADDRESS, lanConfig.ip()); + } + + private void fetchConnectionStatus() throws FreeboxException { + Status status = getManager(ConnectionManager.class).getConfig(); + updateChannelString(CONNECTION_STATUS, LINE_STATUS, status.state()); + updateChannelString(CONNECTION_STATUS, LINE_TYPE, status.type()); + updateChannelString(CONNECTION_STATUS, LINE_MEDIA, status.media()); + updateChannelString(CONNECTION_STATUS, IP_ADDRESS, status.ipv4()); + updateChannelString(CONNECTION_STATUS, IPV6_ADDRESS, status.ipv6()); + + updateRateBandwidth(status.rateUp(), status.bandwidthUp(), "up"); + updateRateBandwidth(status.rateDown(), status.bandwidthDown(), "down"); + + updateChannelQuantity(CONNECTION_STATUS, BYTES_UP, new QuantityType<>(status.bytesUp(), OCTET), GIBIOCTET); + updateChannelQuantity(CONNECTION_STATUS, BYTES_DOWN, new QuantityType<>(status.bytesDown(), OCTET), GIBIOCTET); + } + + private void updateRateBandwidth(long rate, long bandwidth, String orientation) { + QuantityType rateUp = new QuantityType<>(rate * 8, Units.BIT_PER_SECOND); + QuantityType bandwidthUp = new QuantityType<>(bandwidth, BIT_PER_SECOND); + updateChannelQuantity(CONNECTION_STATUS, RATE + "-" + orientation, rateUp, KILOBIT_PER_SECOND); + updateChannelQuantity(CONNECTION_STATUS, BW + "-" + orientation, bandwidthUp, KILOBIT_PER_SECOND); + updateChannelQuantity(CONNECTION_STATUS, PCT_BW + "-" + orientation, + !bandwidthUp.equals(QuantityType.ZERO) ? rateUp.multiply(HUNDRED).divide(bandwidthUp) + : QuantityType.ZERO, + Units.PERCENT); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + if (ON_OFF_CLASSES.contains(command.getClass())) { + boolean enable = TRUE_COMMANDS.contains(command); + switch (channelId) { + case WIFI_STATUS: + updateChannelOnOff(ACTIONS, WIFI_STATUS, getManager(WifiManager.class).setStatus(enable)); + return true; + case FTP_STATUS: + updateChannelOnOff(FILE_SHARING, FTP_STATUS, getManager(FtpManager.class).setStatus(enable)); + return true; + case SAMBA_FILE_STATUS: + updateChannelOnOff(FILE_SHARING, SAMBA_FILE_STATUS, + getManager(SambaManager.class).setFileShare(enable)); + return true; + case SAMBA_PRINTER_STATUS: + updateChannelOnOff(FILE_SHARING, SAMBA_PRINTER_STATUS, + getManager(SambaManager.class).setPrintShare(enable)); + return true; + case UPNPAV_STATUS: + updateChannelOnOff(ACTIONS, UPNPAV_STATUS, getManager(UPnPAVManager.class).setStatus(enable)); + return true; + case AFP_FILE_STATUS: + updateChannelOnOff(FILE_SHARING, AFP_FILE_STATUS, getManager(AfpManager.class).setStatus(enable)); + return true; + case AIRMEDIA_STATUS: + updateChannelOnOff(ACTIONS, AIRMEDIA_STATUS, getManager(AirMediaManager.class).setStatus(enable)); + return true; + default: + break; + } + } + return super.internalHandleCommand(channelId, command); + } + + public void reboot() { + processReboot(() -> { + try { + getManager(SystemManager.class).reboot(); + } catch (FreeboxException e) { + logger.warn("Error rebooting: {}", e.getMessage()); + } + }); + } + + @Override + public Collection> getServices() { + return Collections.singleton(ServerActions.class); + } + + @Override + public ChannelUID getEventChannelUID() { + return eventChannelUID; + } + + @Override + public void triggerChannel(ChannelUID channelUID, String event) { + super.triggerChannel(channelUID, event); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ShutterHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ShutterHandler.java new file mode 100644 index 00000000000..20644ad3408 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/ShutterHandler.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.Endpoint; +import org.openhab.binding.freeboxos.internal.api.rest.HomeManager.EndpointState; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * The {@link ShutterHandler} is responsible for handling everything associated to any Freebox Home shutter. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class ShutterHandler extends HomeNodeHandler { + + public ShutterHandler(Thing thing) { + super(thing); + } + + @Override + protected void internalConfigureChannel(String channelId, Configuration conf, List endpoints) { + endpoints.stream().filter(ep -> channelId.equals(SHUTTER_POSITION) && ep.name().equals(SHUTTER_STOP)) + .forEach(endPoint -> conf.put(endPoint.name(), endPoint.id())); + } + + @Override + protected State getChannelState(HomeManager homeManager, String channelId, EndpointState state) { + String value = state.value(); + return value != null && channelId.equals(SHUTTER_POSITION) ? QuantityType.valueOf(value + " %") + : UnDefType.NULL; + } + + @Override + protected boolean executeSlotCommand(HomeManager homeManager, String channelId, Command command, + Configuration config, int positionSlot) throws FreeboxException { + Integer stopSlot = getSlotId(config, SHUTTER_STOP); + if (SHUTTER_POSITION.equals(channelId) && stopSlot instanceof Integer) { + if (command instanceof UpDownType upDownCmd) { + return operateShutter(homeManager, stopSlot, positionSlot, upDownCmd == UpDownType.DOWN ? 100 : 0); + } else if (command instanceof StopMoveType stopMove && stopMove == StopMoveType.STOP) { + return operateShutter(homeManager, stopSlot, positionSlot, -1); + } else if (command instanceof Number numberCmd) { + return operateShutter(homeManager, stopSlot, positionSlot, numberCmd.intValue()); + } + } + return false; + } + + private boolean operateShutter(HomeManager homeManager, int stopSlot, int positionSlot, int target) + throws FreeboxException { + homeManager.putCommand(getClientId(), stopSlot, true); + if (target >= 0) { + homeManager.putCommand(getClientId(), positionSlot, target); + } + return true; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java new file mode 100644 index 00000000000..2f1ab7b06cf --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/VmHandler.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.VmManager; +import org.openhab.binding.freeboxos.internal.api.rest.VmManager.Status; +import org.openhab.binding.freeboxos.internal.api.rest.VmManager.VirtualMachine; +import org.openhab.binding.freeboxos.internal.api.rest.WebSocketManager; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link VmHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class VmHandler extends HostHandler { + private final Logger logger = LoggerFactory.getLogger(VmHandler.class); + + // We start in pull mode and switch to push after a first update + private boolean pushSubscribed = false; + + public VmHandler(Thing thing) { + super(thing); + } + + @Override + public void dispose() { + try { + getManager(WebSocketManager.class).unregisterVm(getClientId()); + } catch (FreeboxException e) { + logger.warn("Error unregistering VM from the websocket: {}", e.getMessage()); + } + super.dispose(); + } + + @Override + protected void internalPoll() throws FreeboxException { + if (pushSubscribed) { + return; + } + super.internalPoll(); + + logger.debug("Polling Virtual machine status"); + VirtualMachine vm = getManager(VmManager.class).getDevice(getClientId()); + updateVmChannels(vm); + getManager(WebSocketManager.class).registerVm(vm.id(), this); + pushSubscribed = true; + } + + public void updateVmChannels(VirtualMachine vm) { + boolean running = Status.RUNNING.equals(vm.status()); + updateChannelOnOff(VM_STATUS, STATUS, running); + updateChannelOnOff(CONNECTIVITY, REACHABLE, running); + updateStatus(running ? ThingStatus.ONLINE : ThingStatus.OFFLINE); + } + + @Override + protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException { + if (STATUS.equals(channelId) && command instanceof OnOffType) { + getManager(VmManager.class).power(getClientId(), OnOffType.ON.equals(command)); + return true; + } + return super.internalHandleCommand(channelId, command); + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/WifiStationHandler.java b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/WifiStationHandler.java new file mode 100644 index 00000000000..c22c9ad3333 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/java/org/openhab/binding/freeboxos/internal/handler/WifiStationHandler.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.freeboxos.internal.handler; + +import static org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants.*; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.freeboxos.internal.api.FreeboxException; +import org.openhab.binding.freeboxos.internal.api.rest.APManager; +import org.openhab.binding.freeboxos.internal.api.rest.APManager.LanAccessPoint; +import org.openhab.binding.freeboxos.internal.api.rest.APManager.Station; +import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.LanHost; +import org.openhab.binding.freeboxos.internal.api.rest.RepeaterManager; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.UnDefType; + +/** + * The {@link WifiStationHandler} is responsible for handling everything associated to + * any Freebox thing types except the bridge thing type. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public class WifiStationHandler extends HostHandler { + private static final String SERVER_HOST = "Server"; + + public WifiStationHandler(Thing thing) { + super(thing); + } + + @Override + protected void internalPoll() throws FreeboxException { + super.internalPoll(); + + // Search if the wifi-host is hosted on server access-points + Optional station = getManager(APManager.class).getStation(getMac()); + if (station.isPresent()) { + Station data = station.get(); + updateChannelDateTimeState(CONNECTIVITY, LAST_SEEN, data.getLastSeen()); + updateChannelString(GROUP_WIFI, WIFI_HOST, SERVER_HOST); + updateWifiStationChannels(data.signal(), data.getSsid(), data.rxRate(), data.txRate()); + return; + } + + // Search if it is hosted by a repeater + Optional wifiHost = getManager(RepeaterManager.class).getHost(getMac()); + if (wifiHost.isPresent()) { + updateChannelDateTimeState(CONNECTIVITY, LAST_SEEN, wifiHost.get().getLastSeen()); + LanAccessPoint lanAp = wifiHost.get().accessPoint(); + if (lanAp != null) { + updateChannelString(GROUP_WIFI, WIFI_HOST, "%s-%s".formatted(lanAp.type(), lanAp.uid())); + updateWifiStationChannels(lanAp.getSignal(), lanAp.getSsid(), lanAp.rxRate(), lanAp.txRate()); + return; + } + } + // Not found a wifi repeater/host, so update all wifi channels to NULL + getThing().getChannelsOfGroup(GROUP_WIFI).stream().map(Channel::getUID).filter(uid -> isLinked(uid)) + .forEach(uid -> updateState(uid, UnDefType.NULL)); + } + + private void updateWifiStationChannels(int rssi, @Nullable String ssid, long rxRate, long txRate) { + updateChannelString(GROUP_WIFI, SSID, ssid); + updateChannelQuantity(GROUP_WIFI, RSSI, rssi <= 0 ? new QuantityType<>(rssi, Units.DECIBEL_MILLIWATTS) : null); + updateChannelDecimal(GROUP_WIFI, WIFI_QUALITY, rssi <= 0 ? toQoS(rssi) : null); + updateRateChannel(RATE + "-down", rxRate); + updateRateChannel(RATE + "-up", txRate); + } + + private void updateRateChannel(String channel, long rate) { + QuantityType qtty = rate != -1 ? new QuantityType<>(rate * 8, Units.BIT_PER_SECOND) : null; + updateChannelQuantity(GROUP_WIFI, channel, qtty); + } + + private int toQoS(int rssi) { + return rssi > -50 ? 4 : rssi > -60 ? 3 : rssi > -70 ? 2 : rssi > -85 ? 1 : 0; + } +} diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 00000000000..9c9b553c8fe --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,24 @@ + + + + binding + Freebox OS Binding + The Freebox OS binding integrates Free equipments in your home automation. + local + fr,it + + + + + The timeout for reading from the API in seconds. + 8 + + + + URL to use for playing notification sounds hosted by openHAB, e.g. 'http://192.168.0.2:8080' + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml new file mode 100644 index 00000000000..4c5f6bec9c4 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/bridge-config.xml @@ -0,0 +1,39 @@ + + + + + + + network-address + The domain to use in place of hardcoded Freebox ip + mafreebox.freebox.fr + + + + password + Token generated by the Freebox server + + + + Enable the discovery of network device things + false + + + + Tells if https has been configured on the Freebox + true + false + + + + Port to use for remote https access to the Freebox Api + true + 15682 + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/home-node-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/home-node-config.xml new file mode 100644 index 00000000000..e280240bce1 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/home-node-config.xml @@ -0,0 +1,20 @@ + + + + + + + Id of the Home Node + 1 + + + + The refresh interval in seconds which is used to poll the Node + 30 + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/host-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/host-config.xml new file mode 100644 index 00000000000..ac3073219b5 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/host-config.xml @@ -0,0 +1,20 @@ + + + + + + + The refresh interval in seconds which is used to poll given device + 30 + + + + The MAC address of the network device + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/phone-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/phone-config.xml new file mode 100644 index 00000000000..430d95bb06b --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/phone-config.xml @@ -0,0 +1,30 @@ + + + + + + + The refresh interval in seconds which is used to poll for phone state. + 30 + + + + Id of the phone line + 1 + + + + + + + The refresh interval in seconds which is used to poll for phone state. + 2 + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/player-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/player-config.xml new file mode 100644 index 00000000000..77eb76882ba --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/player-config.xml @@ -0,0 +1,46 @@ + + + + + + + The MAC address of the player device + + + + Id of the player + 1 + + + + 24322 + true + + + password + + AirPlay password + true + + + + Code associated to remote control + true + + + + Accept any bitrate for MP3 audio or only bitrates greater than 64 kbps + true + true + + + + The refresh interval in seconds which is used to poll the player + 30 + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/repeater-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/repeater-config.xml new file mode 100644 index 00000000000..b78989608cc --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/repeater-config.xml @@ -0,0 +1,25 @@ + + + + + + + The refresh interval in seconds which is used to poll the repeater + 30 + + + + The MAC address of the network device + + + + Id of the repeater + 1 + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/server-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/server-config.xml new file mode 100644 index 00000000000..7bdfad57d14 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/server-config.xml @@ -0,0 +1,20 @@ + + + + + + + The refresh interval in seconds which is used to poll given Freebox Server + 30 + + + + The MAC address of the network device + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/vm-config.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/vm-config.xml new file mode 100644 index 00000000000..46fad86b26a --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/config/vm-config.xml @@ -0,0 +1,24 @@ + + + + + + + The refresh interval in seconds which is used to poll given virtual machine + 30 + + + + The MAC address of the network device + + + + Id of the Virtual Machine + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties new file mode 100644 index 00000000000..86b59db4e61 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/i18n/freeboxos.properties @@ -0,0 +1,354 @@ +# add-on + +addon.freeboxos.name = Freebox OS Binding +addon.freeboxos.description = The Freebox OS binding integrates Free equipments in your home automation. + +# add-on config + +addon.config.freeboxos.callbackUrl.label = Callback URL +addon.config.freeboxos.callbackUrl.description = URL to use for playing notification sounds hosted by openHAB, e.g. 'http://192.168.0.2:8080' +addon.config.freeboxos.timeout.label = Timeout +addon.config.freeboxos.timeout.description = The timeout for reading from the API in seconds. + +# thing types + +thing-type.freeboxos.active-player.label = Freebox Player +thing-type.freeboxos.active-player.description = The player is the device connected to your TV with API capabilities +thing-type.freeboxos.alarm.label = Freebox Alarm +thing-type.freeboxos.alarm.description = The Alarm system configured in your Freebox Delta Server +thing-type.freeboxos.alarm.channel.sound.label = Bips Volume +thing-type.freeboxos.alarm.channel.timeout1.label = Alarm Activation Duration +thing-type.freeboxos.alarm.channel.timeout2.label = Safe Zone Alert Timeout +thing-type.freeboxos.alarm.channel.timeout3.label = Alert Duration +thing-type.freeboxos.alarm.channel.volume.label = Alarm Volume +thing-type.freeboxos.api.label = Freebox OS Api +thing-type.freeboxos.api.description = Bridge between hosts and the API rest service +thing-type.freeboxos.basic-shutter.label = Freebox Home Basic Shutter +thing-type.freeboxos.basic-shutter.description = The Basic Shutter (UP,DOWN,STOP) configured in your Freebox Delta Server +thing-type.freeboxos.call.label = Calls +thing-type.freeboxos.call.description = Provides various informations regarding the phone calls +thing-type.freeboxos.dect.label = DECT +thing-type.freeboxos.dect.description = Provides various informations regarding the DECT state and configuration +thing-type.freeboxos.dect.channel.gain-rx.label = Gain RX +thing-type.freeboxos.dect.channel.gain-tx.label = Gain TX +thing-type.freeboxos.delta.label = Freebox Delta +thing-type.freeboxos.delta.description = Provides various informations regarding the status of the Freebox Delta Server +thing-type.freeboxos.freeplug.label = Freeplug +thing-type.freeboxos.freeplug.description = Ethernet / CPL gateway +thing-type.freeboxos.freeplug.channel.last-seen.label = Last Activity +thing-type.freeboxos.freeplug.channel.rate-down.label = Rx Rate +thing-type.freeboxos.freeplug.channel.rate-down.description = Current RX rate +thing-type.freeboxos.freeplug.channel.rate-up.label = Tx Rate +thing-type.freeboxos.freeplug.channel.rate-up.description = Current TX Rate +thing-type.freeboxos.fxs.label = Landline +thing-type.freeboxos.fxs.description = Provides various informations regarding the landline state +thing-type.freeboxos.host.label = Network Device +thing-type.freeboxos.host.description = Provides network device reachability +thing-type.freeboxos.kfb.label = Freebox Keyfob +thing-type.freeboxos.kfb.description = A keyfob configured for your Freebox Security system +thing-type.freeboxos.player.label = Freebox Player +thing-type.freeboxos.player.description = The player is the device connected to your TV +thing-type.freeboxos.repeater.label = Wifi Repeater +thing-type.freeboxos.repeater.description = Provides informations and control over a Wifi Repeater +thing-type.freeboxos.revolution.label = Freebox Revolution +thing-type.freeboxos.revolution.description = Provides various informations regarding the status of the Freebox Revolution Server +thing-type.freeboxos.shutter.label = Freebox Home Shutter +thing-type.freeboxos.shutter.description = The Shutter configured in your Freebox Delta Server +thing-type.freeboxos.vm.label = Virtual Machine +thing-type.freeboxos.vm.description = Provides informations and control over virtual machine hosted on the server +thing-type.freeboxos.wifihost.label = Wifi Device +thing-type.freeboxos.wifihost.description = Provides Wifi device reachability + +# thing types config + +bridge-type.config.freeboxos.api.apiDomain.label = Freebox Server Address +bridge-type.config.freeboxos.api.apiDomain.description = The domain to use in place of hardcoded Freebox ip +bridge-type.config.freeboxos.api.appToken.label = Application Token +bridge-type.config.freeboxos.api.appToken.description = Token generated by the Freebox server +bridge-type.config.freeboxos.api.discoverNetDevice.label = Network Device Discovery +bridge-type.config.freeboxos.api.discoverNetDevice.description = Enable the discovery of network device things +bridge-type.config.freeboxos.api.httpsAvailable.label = HTTPS Available +bridge-type.config.freeboxos.api.httpsAvailable.description = Tells if https has been configured on the Freebox +bridge-type.config.freeboxos.api.httpsPort.label = HTTPS port +bridge-type.config.freeboxos.api.httpsPort.description = Port to use for remote https access to the Freebox Api +thing-type.config.freeboxos.call.refreshInterval.label = State Refresh Interval +thing-type.config.freeboxos.call.refreshInterval.description = The refresh interval in seconds which is used to poll for phone state. +thing-type.config.freeboxos.home-node.id.label = ID +thing-type.config.freeboxos.home-node.id.description = Id of the Home Node +thing-type.config.freeboxos.home-node.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.home-node.refreshInterval.description = The refresh interval in seconds which is used to poll the Node +thing-type.config.freeboxos.host.macAddress.label = MAC Address +thing-type.config.freeboxos.host.macAddress.description = The MAC address of the network device +thing-type.config.freeboxos.host.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.host.refreshInterval.description = The refresh interval in seconds which is used to poll given device +thing-type.config.freeboxos.phone.id.label = ID +thing-type.config.freeboxos.phone.id.description = Id of the phone line +thing-type.config.freeboxos.phone.refreshInterval.label = State Refresh Interval +thing-type.config.freeboxos.phone.refreshInterval.description = The refresh interval in seconds which is used to poll for phone state. +thing-type.config.freeboxos.player.acceptAllMp3.label = Accept All MP3 +thing-type.config.freeboxos.player.acceptAllMp3.description = Accept any bitrate for MP3 audio or only bitrates greater than 64 kbps +thing-type.config.freeboxos.player.id.label = ID +thing-type.config.freeboxos.player.id.description = Id of the player +thing-type.config.freeboxos.player.macAddress.label = MAC Address +thing-type.config.freeboxos.player.macAddress.description = The MAC address of the player device +thing-type.config.freeboxos.player.password.label = Password +thing-type.config.freeboxos.player.password.description = AirPlay password +thing-type.config.freeboxos.player.port.label = Player port +thing-type.config.freeboxos.player.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.player.refreshInterval.description = The refresh interval in seconds which is used to poll the player +thing-type.config.freeboxos.player.remoteCode.label = Remote Code +thing-type.config.freeboxos.player.remoteCode.description = Code associated to remote control +thing-type.config.freeboxos.repeater.id.label = ID +thing-type.config.freeboxos.repeater.id.description = Id of the repeater +thing-type.config.freeboxos.repeater.macAddress.label = MAC Address +thing-type.config.freeboxos.repeater.macAddress.description = The MAC address of the network device +thing-type.config.freeboxos.repeater.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.repeater.refreshInterval.description = The refresh interval in seconds which is used to poll the repeater +thing-type.config.freeboxos.server.macAddress.label = MAC Address +thing-type.config.freeboxos.server.macAddress.description = The MAC address of the network device +thing-type.config.freeboxos.server.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.server.refreshInterval.description = The refresh interval in seconds which is used to poll given Freebox Server +thing-type.config.freeboxos.vm.id.label = ID +thing-type.config.freeboxos.vm.id.description = Id of the Virtual Machine +thing-type.config.freeboxos.vm.macAddress.label = MAC Address +thing-type.config.freeboxos.vm.macAddress.description = The MAC address of the network device +thing-type.config.freeboxos.vm.refreshInterval.label = Refresh Interval +thing-type.config.freeboxos.vm.refreshInterval.description = The refresh interval in seconds which is used to poll given virtual machine + +# channel group types + +channel-group-type.freeboxos.accepted.label = Accepted Call +channel-group-type.freeboxos.accepted.description = The last accepted phone call +channel-group-type.freeboxos.accepted.channel.duration.label = Incoming Call Duration +channel-group-type.freeboxos.accepted.channel.name.label = Accepted Caller +channel-group-type.freeboxos.accepted.channel.name.description = Caller name +channel-group-type.freeboxos.accepted.channel.number.label = Calling Number +channel-group-type.freeboxos.accepted.channel.number.description = Caller phone number +channel-group-type.freeboxos.accepted.channel.timestamp.label = Incoming Call Timestamp +channel-group-type.freeboxos.actions.label = Server Settings +channel-group-type.freeboxos.connection-status.label = Connection Status Details +channel-group-type.freeboxos.connection-status.channel.bandwidth-down.label = Bandwidth Down +channel-group-type.freeboxos.connection-status.channel.bandwidth-down.description = Raw value of the download bandwidth currently used +channel-group-type.freeboxos.connection-status.channel.bandwidth-up.label = Bandwidth Up +channel-group-type.freeboxos.connection-status.channel.bandwidth-up.description = Raw value of the upload bandwidth currently used +channel-group-type.freeboxos.connection-status.channel.bandwidth-usage-down.label = Bandwidth Usage Down +channel-group-type.freeboxos.connection-status.channel.bandwidth-usage-down.description = Portion of the download bandwidth currently used +channel-group-type.freeboxos.connection-status.channel.bandwidth-usage-up.label = Bandwidth Usage Up +channel-group-type.freeboxos.connection-status.channel.bandwidth-usage-up.description = Portion of the upload bandwidth currently used +channel-group-type.freeboxos.connection-status.channel.bytes-down.label = Downloaded +channel-group-type.freeboxos.connection-status.channel.bytes-down.description = Total data downloaded since last restart +channel-group-type.freeboxos.connection-status.channel.bytes-up.label = Uploaded +channel-group-type.freeboxos.connection-status.channel.bytes-up.description = Total data uploaded since last restart +channel-group-type.freeboxos.connection-status.channel.ip-address.label = Public IPv4 +channel-group-type.freeboxos.connection-status.channel.ip-address.description = Public IPv4 Address of the Freebox (this field is only available when connection state is up) +channel-group-type.freeboxos.connection-status.channel.ipv6-address.label = Public IPv6 +channel-group-type.freeboxos.connection-status.channel.ipv6-address.description = Public IPv6 Address of the Freebox (this field is only available when connection state is up) +channel-group-type.freeboxos.connection-status.channel.rate-down.label = Download Rate +channel-group-type.freeboxos.connection-status.channel.rate-down.description = Current download rate +channel-group-type.freeboxos.connection-status.channel.rate-up.label = Upload Rate +channel-group-type.freeboxos.connection-status.channel.rate-up.description = Current upload rate +channel-group-type.freeboxos.connectivity.label = Network Connectivity +channel-group-type.freeboxos.connectivity.channel.ip-address.label = IP Address +channel-group-type.freeboxos.connectivity.channel.ip-address.description = IPv4 Address of the host +channel-group-type.freeboxos.connectivity.channel.last-seen.label = Last Activity +channel-group-type.freeboxos.display.label = Front Display Panel +channel-group-type.freeboxos.fans.label = Fans +channel-group-type.freeboxos.file-sharing.label = File Sharing +channel-group-type.freeboxos.incoming.label = Incoming Call +channel-group-type.freeboxos.incoming.description = Currently presented phone call +channel-group-type.freeboxos.incoming.channel.name.label = Incoming Caller +channel-group-type.freeboxos.incoming.channel.name.description = Caller name +channel-group-type.freeboxos.incoming.channel.number.label = Calling Number +channel-group-type.freeboxos.incoming.channel.number.description = Caller phone number +channel-group-type.freeboxos.incoming.channel.timestamp.label = Call Timestamp +channel-group-type.freeboxos.missed.label = Missed Call +channel-group-type.freeboxos.missed.description = The last missed phone call +channel-group-type.freeboxos.missed.channel.name.label = Missed Caller +channel-group-type.freeboxos.missed.channel.name.description = Caller name +channel-group-type.freeboxos.missed.channel.number.label = Missed Calling Number +channel-group-type.freeboxos.missed.channel.number.description = Caller phone number +channel-group-type.freeboxos.missed.channel.timestamp.label = Missed Call Timestamp +channel-group-type.freeboxos.outgoing.label = Outgoing Call +channel-group-type.freeboxos.outgoing.description = The last outgoing phone call +channel-group-type.freeboxos.outgoing.channel.duration.label = Outgoing Call Duration +channel-group-type.freeboxos.outgoing.channel.name.label = Called Name +channel-group-type.freeboxos.outgoing.channel.name.description = Name, if known, of the called person +channel-group-type.freeboxos.outgoing.channel.number.label = Called Number +channel-group-type.freeboxos.outgoing.channel.number.description = Called phone number +channel-group-type.freeboxos.outgoing.channel.timestamp.label = Outgoing Call Timestamp +channel-group-type.freeboxos.player-actions.label = Player Actions +channel-group-type.freeboxos.player-status.label = Player Status +channel-group-type.freeboxos.player-sysinfo.label = System Informations +channel-group-type.freeboxos.repeater-misc.label = Repeater Settings +channel-group-type.freeboxos.repeater-misc.channel.box-event.label = Repeater Event +channel-group-type.freeboxos.sensors.label = System Sensors +channel-group-type.freeboxos.sysinfo.label = System Informations +channel-group-type.freeboxos.sysinfo.channel.ip-address.label = Internal IP +channel-group-type.freeboxos.sysinfo.channel.ip-address.description = Internal IPv4 Address of the host +channel-group-type.freeboxos.vmstatus.label = VM Status +channel-group-type.freeboxos.wifi.label = Wifi Related Information +channel-group-type.freeboxos.wifi.channel.rate-down.label = Rx Rate +channel-group-type.freeboxos.wifi.channel.rate-down.description = Current RX rate +channel-group-type.freeboxos.wifi.channel.rate-up.label = Tx Rate +channel-group-type.freeboxos.wifi.channel.rate-up.description = Current TX Rate + +# channel types + +channel-type.freeboxos.afp-file-status.label = Mac OS File Sharing +channel-type.freeboxos.afp-file-status.description = Status of Mac OS File Sharing +channel-type.freeboxos.airmedia-status.label = Air Media Enabled +channel-type.freeboxos.airmedia-status.description = Indicates whether Air Media is enabled +channel-type.freeboxos.alarm-pin.label = PIN Code +channel-type.freeboxos.alarm-timeout.label = Alarm Duration +channel-type.freeboxos.alarm-volume.label = Alarm Volume +channel-type.freeboxos.alternate-ring.label = Alternating Ring +channel-type.freeboxos.bandwidth-usage.label = Bandwidth Usage +channel-type.freeboxos.bandwidth-usage.description = Current bandwidth usage +channel-type.freeboxos.bandwidth.label = Bandwidth +channel-type.freeboxos.bandwidth.description = Available bandwidth +channel-type.freeboxos.basic-shutter.label = Shutter +channel-type.freeboxos.basic-shutter.description = Shutter command +channel-type.freeboxos.box-event.label = Server Event +channel-type.freeboxos.box-event.description = Triggers when an event related to the Freebox server has been detected +channel-type.freeboxos.connection-status.label = Connection +channel-type.freeboxos.connection-status.description = Connection Status +channel-type.freeboxos.dect-active.label = DECT Enabled +channel-type.freeboxos.dect-active.description = Activates / stops the integrated DECT base +channel-type.freeboxos.duration.label = Duration +channel-type.freeboxos.duration.description = Call duration in seconds +channel-type.freeboxos.fanspeed.label = Fan Speed +channel-type.freeboxos.fanspeed.description = Actual measured rotation speed of the fan +channel-type.freeboxos.ftp-status.label = FTP Server Enabled +channel-type.freeboxos.ftp-status.description = Indicates whether the FTP server is enabled +channel-type.freeboxos.gain.label = Gain +channel-type.freeboxos.hardware-status.label = Hardware Status +channel-type.freeboxos.hardware-status.description = Hardware status of the phone line +channel-type.freeboxos.host-count.label = Host Count +channel-type.freeboxos.host-count.description = Number of hosts connected to the device +channel-type.freeboxos.ip-address.label = IP Address +channel-type.freeboxos.ip-address.description = IP address of the client +channel-type.freeboxos.key-code.label = Remote Key Code +channel-type.freeboxos.key-code.description = Simulates pushing a remote control button +channel-type.freeboxos.key-code.state.option.red = Red +channel-type.freeboxos.key-code.state.option.green = Green +channel-type.freeboxos.key-code.state.option.blue = Blue +channel-type.freeboxos.key-code.state.option.yellow = Yellow +channel-type.freeboxos.key-code.state.option.power = On/Off +channel-type.freeboxos.key-code.state.option.list = List +channel-type.freeboxos.key-code.state.option.tv = TV +channel-type.freeboxos.key-code.state.option.0 = 0 +channel-type.freeboxos.key-code.state.option.1 = 1 +channel-type.freeboxos.key-code.state.option.2 = 2 +channel-type.freeboxos.key-code.state.option.3 = 3 +channel-type.freeboxos.key-code.state.option.4 = 4 +channel-type.freeboxos.key-code.state.option.5 = 5 +channel-type.freeboxos.key-code.state.option.6 = 6 +channel-type.freeboxos.key-code.state.option.7 = 7 +channel-type.freeboxos.key-code.state.option.8 = 8 +channel-type.freeboxos.key-code.state.option.9 = 9 +channel-type.freeboxos.key-code.state.option.vol_inc = Volume Up +channel-type.freeboxos.key-code.state.option.vol_dec = Volume Down +channel-type.freeboxos.key-code.state.option.mute = Mute +channel-type.freeboxos.key-code.state.option.prgm_inc = Prog Up +channel-type.freeboxos.key-code.state.option.prgm_dec = Prog Down +channel-type.freeboxos.key-code.state.option.prev = Previous +channel-type.freeboxos.key-code.state.option.bwd = Backward +channel-type.freeboxos.key-code.state.option.play = Play/Pause +channel-type.freeboxos.key-code.state.option.rec = Record +channel-type.freeboxos.key-code.state.option.fwd = Forward +channel-type.freeboxos.key-code.state.option.next = Next +channel-type.freeboxos.key-code.state.option.up = Up +channel-type.freeboxos.key-code.state.option.right = Right +channel-type.freeboxos.key-code.state.option.down = Down +channel-type.freeboxos.key-code.state.option.left = Left +channel-type.freeboxos.key-code.state.option.back = Back +channel-type.freeboxos.key-code.state.option.swap = Swap +channel-type.freeboxos.key-code.state.option.info = Info +channel-type.freeboxos.key-code.state.option.epg = EPG +channel-type.freeboxos.key-code.state.option.mail = Mail +channel-type.freeboxos.key-code.state.option.media = Media +channel-type.freeboxos.key-code.state.option.help = Help +channel-type.freeboxos.key-code.state.option.options = Options +channel-type.freeboxos.key-code.state.option.pip = PIP +channel-type.freeboxos.key-code.state.option.ok = OK +channel-type.freeboxos.key-code.state.option.home = Home +channel-type.freeboxos.keyfob-enable.label = Keyfob Enabled +channel-type.freeboxos.keyfob-enable.description = Activates / deactivates the keyfob +channel-type.freeboxos.lcd-brightness.label = Screen Brightness +channel-type.freeboxos.lcd-brightness.description = Brightness level of the screen in percent +channel-type.freeboxos.lcd-forced.label = Forced Orientation +channel-type.freeboxos.lcd-forced.description = Indicates whether the screen orientation is forced +channel-type.freeboxos.lcd-orientation.label = Screen Orientation +channel-type.freeboxos.lcd-orientation.description = Screen Orientation in degrees +channel-type.freeboxos.lcd-orientation.state.option.0 = Horizontal +channel-type.freeboxos.lcd-orientation.state.option.90 = Turned left +channel-type.freeboxos.lcd-orientation.state.option.180 = Reversed +channel-type.freeboxos.lcd-orientation.state.option.270 = Turned right +channel-type.freeboxos.led.label = Led Activated +channel-type.freeboxos.led.description = Led indicator status +channel-type.freeboxos.line-media.label = Line Media +channel-type.freeboxos.line-media.description = Media of network line connection +channel-type.freeboxos.line-media.state.option.FTTH = FTTH +channel-type.freeboxos.line-media.state.option.ETHERNET = Ethernet +channel-type.freeboxos.line-media.state.option.XDSL = xDSL +channel-type.freeboxos.line-media.state.option.BACKUP_4G = Internet Backup +channel-type.freeboxos.line-status.label = Line Status +channel-type.freeboxos.line-status.description = Status of network line connection +channel-type.freeboxos.line-status.state.option.GOING_UP = Connection is initializing +channel-type.freeboxos.line-status.state.option.UP = Connection is active +channel-type.freeboxos.line-status.state.option.GOING_DOWN = Connection is about to become inactive +channel-type.freeboxos.line-status.state.option.DOWN = Connection is inactive +channel-type.freeboxos.line-type.label = Line Type +channel-type.freeboxos.line-type.description = Type of network line connection +channel-type.freeboxos.line-type.state.option.ETHERNET = FTTH/ethernet +channel-type.freeboxos.line-type.state.option.RFC2684 = xDSL (unbundled) +channel-type.freeboxos.line-type.state.option.PPPOATM = xDSL +channel-type.freeboxos.name.label = Name +channel-type.freeboxos.name.description = Called name for outgoing calls. Caller name for incoming calls +channel-type.freeboxos.number.label = Incoming Call +channel-type.freeboxos.number.description = Details about call +channel-type.freeboxos.onhook.label = On Hook +channel-type.freeboxos.onhook.description = Indicates whether the phone is on hook +channel-type.freeboxos.package.label = Active Package +channel-type.freeboxos.package.description = Name of the package currently active on the player +channel-type.freeboxos.phone-event.label = Phone Event +channel-type.freeboxos.phone-event.description = Triggers when an event related to the phone has been detected +channel-type.freeboxos.phone-number.label = Phone Number +channel-type.freeboxos.player-status.label = Player Status +channel-type.freeboxos.player-status.description = Status of the Freebox TV player +channel-type.freeboxos.reachable.label = Reachable +channel-type.freeboxos.reachable.description = Indicates if the network device is reachable or not +channel-type.freeboxos.ringing.label = Ringing +channel-type.freeboxos.ringing.description = Is the phone ringing +channel-type.freeboxos.rssi.label = RSSI +channel-type.freeboxos.rssi.description = Received signal strength indicator +channel-type.freeboxos.samba-file-status.label = Windows File Sharing +channel-type.freeboxos.samba-file-status.description = Status of Windows File Sharing (Samba) +channel-type.freeboxos.samba-printer-status.label = Windows Printer Sharing +channel-type.freeboxos.samba-printer-status.description = Status of Windows Printer Sharing +channel-type.freeboxos.shutter.label = Shutter Position +channel-type.freeboxos.shutter.description = Read / Write position of the shutter +channel-type.freeboxos.ssid.label = SSID +channel-type.freeboxos.status.label = VM Status +channel-type.freeboxos.telephony-service.label = Telephony Service +channel-type.freeboxos.telephony-service.description = Status of the telephony service +channel-type.freeboxos.temperature.label = Temperature +channel-type.freeboxos.temperature.description = Actual measured temperature of the sensor +channel-type.freeboxos.timestamp.label = Timestamp +channel-type.freeboxos.transfer-bytes.label = Transfered Bytes +channel-type.freeboxos.transfer-bytes.description = Total data transfered since last connection +channel-type.freeboxos.transfer-rate-bps.label = Transfer Rate +channel-type.freeboxos.transfer-rate-bps.description = Current transfer rate +channel-type.freeboxos.transfer-rate.label = Transfer Rate +channel-type.freeboxos.transfer-rate.description = Current transfer rate +channel-type.freeboxos.upnpav-status.label = UPnP AV Enabled +channel-type.freeboxos.upnpav-status.description = Indicates whether UPnP AV is enabled +channel-type.freeboxos.uptime.label = Uptime +channel-type.freeboxos.uptime.description = Time since last reboot of the equipment +channel-type.freeboxos.wifi-host.label = Access Point +channel-type.freeboxos.wifi-status.label = Wifi Enabled +channel-type.freeboxos.wifi-status.description = Indicates whether the wifi network is enabled + +# messages + +info-conf-pending = Please accept pairing request directly on your freebox diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/api-bridge-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/api-bridge-type.xml new file mode 100644 index 00000000000..cf0f95dc69c --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/api-bridge-type.xml @@ -0,0 +1,16 @@ + + + + + + Bridge between hosts and the API rest service + + apiDomain + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 00000000000..b785f830e0c --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,465 @@ + + + + + Number:Dimensionless + + Brightness level of the screen in percent + DimmableLight + + + + + Number + + Screen Orientation in degrees + + + + + + + + + + + + Number:Power + + Received signal strength indicator + QualityOfService + + + + + String + + + + + + String + + + + + + Switch + + Indicates whether the screen orientation is forced + Switch + + + + Switch + + Activates / stops the integrated DECT base + Switch + + + + Switch + + Switch + + + + Number:Temperature + + Actual measured temperature of the sensor + Temperature + + + + + Number:Frequency + + Actual measured rotation speed of the fan + Fan + + + + + Switch + + Status of Windows File Sharing (Samba) + Switch + + + + Switch + + Status of Mac OS File Sharing + Switch + + + + Switch + + Status of Windows Printer Sharing + Switch + + + + Number:Dimensionless + + Current bandwidth usage + + + + + Number:DataTransferRate + + Current transfer rate + + + + + Number:DataTransferRate + + Current transfer rate + + + + + Number:DataAmount + + Total data transfered since last connection + + + + + Number:DataTransferRate + + Available bandwidth + + + + + Number:Time + + Time since last reboot of the equipment + time + + + + + String + + Status of network line connection + + + + + + + + + + + + String + + Type of network line connection + + + + + + + + + + + String + + Media of network line connection + + + + + + + + + + + + String + + Status of the Freebox TV player + + + + + String + + Name of the package currently active on the player + + + + + Switch + + Indicates whether the wifi network is enabled + Switch + + + + Switch + + Indicates whether the FTP server is enabled + Switch + + + + Switch + + Indicates whether Air Media is enabled + Switch + + + + Switch + + Indicates whether UPnP AV is enabled + Switch + + + + Switch + + Indicates whether the phone is on hook + Switch + + + + + Switch + + Is the phone ringing + Alarm + + + + String + + + + + + Call + + Details about call + + + + + Number:Time + + Call duration in seconds + time + + + + + DateTime + + time + + + + + String + + Called name for outgoing calls. Caller name for incoming calls + + + + + Switch + + Indicates if the network device is reachable or not + Switch + + + + + Switch + + + + + trigger + + Triggers when an event related to the Freebox server has been detected + + + + trigger + + Triggers when an event related to the phone has been detected + + + + String + + Simulates pushing a remote control button + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + veto + + + + String + + IP address of the client + + + + + Switch + + Led indicator status + Switch + + + + Dimmer + + + + + String + + Connection Status + + + + + String + + Hardware status of the phone line + + + + + String + + Status of the telephony service + + + + + Number + + Number of hosts connected to the device + + + + + Switch + + Activates / deactivates the keyfob + Switch + + + + + + + + Rollershutter + + Shutter command + Blinds + + + + + + + + + + Rollershutter + + Read / Write position of the shutter + + + + + + + + + + Number:Time + + oh:freeboxos:zone_temporisee + + + + + Number:Dimensionless + + oh:freeboxos:sirene + + + + + String + + oh:freeboxos:pin_code + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/freeplug-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/freeplug-thing-type.xml new file mode 100644 index 00000000000..89e5e3187fc --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/freeplug-thing-type.xml @@ -0,0 +1,36 @@ + + + + + + + + + + Ethernet / CPL gateway + + + + + + + + + + Current TX Rate + + + + Current RX rate + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/home-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/home-thing-type.xml new file mode 100644 index 00000000000..6c081fa6fb2 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/home-thing-type.xml @@ -0,0 +1,92 @@ + + + + + + + + + + The Basic Shutter (UP,DOWN,STOP) configured in your Freebox Delta Server + + + + + + id + + + + + + + + + + + The Alarm system configured in your Freebox Delta Server + + + + + + + + + + + + + + + + + + + + + + id + + + + + + + + + + + + A keyfob configured for your Freebox Security system + + + + + + + id + + + + + + + + + + + The Shutter configured in your Freebox Delta Server + + + + + + id + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-channel-groups.xml new file mode 100644 index 00000000000..cfa69802776 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-channel-groups.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + IPv4 Address of the host + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-thing-type.xml new file mode 100644 index 00000000000..e1db3093ad4 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/host-thing-type.xml @@ -0,0 +1,24 @@ + + + + + + + + + + Provides network device reachability + + + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-channel-groups.xml new file mode 100644 index 00000000000..b93b4c35da1 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-channel-groups.xml @@ -0,0 +1,85 @@ + + + + + + The last accepted phone call + + + + Caller phone number + + + + + + + + + + Caller name + + + + + + + The last missed phone call + + + + Caller phone number + + + + + + + Caller name + + + + + + + Currently presented phone call + + + + Caller phone number + + + + + + + Caller name + + + + + + + The last outgoing phone call + + + + Called phone number + + + + + + + + + + Name, if known, of the called person + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-thing-type.xml new file mode 100644 index 00000000000..2a0e53b46da --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/phone-thing-type.xml @@ -0,0 +1,72 @@ + + + + + + + + + + Provides various informations regarding the landline state + + + + + + + + + id + + + + + + + + + + + Provides various informations regarding the DECT state and configuration + + + + + + + + + + + + + + + + id + + + + + + + + + + + Provides various informations regarding the phone calls + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-channel-groups.xml new file mode 100644 index 00000000000..9a4f7e2d975 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-channel-groups.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-thing-type.xml new file mode 100644 index 00000000000..e6c94f1194a --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/player-thing-type.xml @@ -0,0 +1,46 @@ + + + + + + + + + + The player is the device connected to your TV + + + + + + + + macAddress + + + + + + + + + + + The player is the device connected to your TV with API capabilities + + + + + + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-channel-groups.xml new file mode 100644 index 00000000000..ea74f3d6fb3 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-channel-groups.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-thing-type.xml new file mode 100644 index 00000000000..326c98ba4fe --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/repeater-thing-type.xml @@ -0,0 +1,25 @@ + + + + + + + + + + Provides informations and control over a Wifi Repeater + + + + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-channel-groups.xml new file mode 100644 index 00000000000..50ed9b4a6f7 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-channel-groups.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + Internal IPv4 Address of the host + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Public IPv4 Address of the Freebox (this field is only available when connection state is up) + + + + Public IPv6 Address of the Freebox (this field is only available when connection state is up) + + + + Raw value of the upload bandwidth currently used + + + + Raw value of the download bandwidth currently used + + + + Portion of the upload bandwidth currently used + + + + Portion of the download bandwidth currently used + + + + Current upload rate + + + + Current download rate + + + + Total data uploaded since last restart + + + + Total data downloaded since last restart + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-thing-type.xml new file mode 100644 index 00000000000..faafbb24b38 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/server-thing-type.xml @@ -0,0 +1,48 @@ + + + + + + + + + + Provides various informations regarding the status of the Freebox Revolution Server + + + + + + + + + + + + + + + + + + + + + Provides various informations regarding the status of the Freebox Delta Server + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-channel-groups.xml new file mode 100644 index 00000000000..6c3092cd1bc --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-channel-groups.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-thing-type.xml new file mode 100644 index 00000000000..ac5a742e770 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/vm-thing-type.xml @@ -0,0 +1,25 @@ + + + + + + + + + + Provides informations and control over virtual machine hosted on the server + + + + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifi-channel-groups.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifi-channel-groups.xml new file mode 100644 index 00000000000..fb52a35865b --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifi-channel-groups.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + Current TX Rate + + + + Current RX rate + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifihost-thing-type.xml b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifihost-thing-type.xml new file mode 100644 index 00000000000..581ba2c50cf --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/OH-INF/thing/wifihost-thing-type.xml @@ -0,0 +1,25 @@ + + + + + + + + + + Provides Wifi device reachability + + + + + + + macAddress + + + + + diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/freeboxECCRootCA.crt b/bundles/org.openhab.binding.freeboxos/src/main/resources/freeboxECCRootCA.crt new file mode 100644 index 00000000000..eab91f680f8 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/freeboxECCRootCA.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIJAMaRcLnIgyukMAoGCCqGSM49BAMCMGExCzAJBgNVBAYT +AkZSMQ8wDQYDVQQIDAZGcmFuY2UxDjAMBgNVBAcMBVBhcmlzMRMwEQYDVQQKDApG +cmVlYm94IFNBMRwwGgYDVQQDDBNGcmVlYm94IEVDQyBSb290IENBMB4XDTE1MDkw +MTE4MDIwN1oXDTM1MDgyNzE4MDIwN1owYTELMAkGA1UEBhMCRlIxDzANBgNVBAgM +BkZyYW5jZTEOMAwGA1UEBwwFUGFyaXMxEzARBgNVBAoMCkZyZWVib3ggU0ExHDAa +BgNVBAMME0ZyZWVib3ggRUNDIFJvb3QgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASCjD6ZKn5ko6cU5Vxh8GA1KqRi6p2GQzndxHtuUmwY8RvBbhZ0GIL7bQ4f08ae +JOv0ycWjEW0fyOnAw6AYdsN6y1eNvH2DVfoXQyGoCSvXQNAUxla+sJuLGICRYiZz +mnijYzBhMB0GA1UdDgQWBBTIB3c2GlbV6EIh2ErEMJvFxMz/QTAfBgNVHSMEGDAW +gBTIB3c2GlbV6EIh2ErEMJvFxMz/QTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBhjAKBggqhkjOPQQDAgNoADBlAjA8tzEMRVX8vrFuOGDhvZr7OSJjbBr8 +gl2I70LeVNGEXZsAThUkqj5Rg9bV8xw3aSMCMQCDjB5CgsLH8EdZmiksdBRRKM2r +vxo6c0dSSNrr7dDN+m2/dRvgoIpGL2GauOGqDFY= +-----END CERTIFICATE----- diff --git a/bundles/org.openhab.binding.freeboxos/src/main/resources/iliadboxECCRootCA.crt b/bundles/org.openhab.binding.freeboxos/src/main/resources/iliadboxECCRootCA.crt new file mode 100644 index 00000000000..12bcc6071f2 --- /dev/null +++ b/bundles/org.openhab.binding.freeboxos/src/main/resources/iliadboxECCRootCA.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIUI0Tu7zsrBJACQIZgLMJobtbdNn4wCgYIKoZIzj0EAwIw +TDELMAkGA1UEBhMCSVQxDjAMBgNVBAgMBUl0YWx5MQ4wDAYDVQQKDAVJbGlhZDEd +MBsGA1UEAwwUSWxpYWRib3ggRUNDIFJvb3QgQ0EwHhcNMjAxMTI3MDkzODEzWhcN +NDAxMTIyMDkzODEzWjBMMQswCQYDVQQGEwJJVDEOMAwGA1UECAwFSXRhbHkxDjAM +BgNVBAoMBUlsaWFkMR0wGwYDVQQDDBRJbGlhZGJveCBFQ0MgUm9vdCBDQTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMryJyb2loHNAioY8IztN5MI3UgbVHVP/vZwcnre +ZvJOyDvE4HJgIti5qmfswlnMzpNbwf/MkT+7HAU8jJoTorRm1wtAnQ9cWD3Ebv79 +RPwtjjy3Bza3SgdVxmd6fWPUKaNjMGEwHQYDVR0OBBYEFDUij/4lpoJ+kOXRyrcM +jf2RPzOqMB8GA1UdIwQYMBaAFDUij/4lpoJ+kOXRyrcMjf2RPzOqMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQC6eUV1 +pFh4UpJOTc1JToztN4ttnQR6rIzxMZ6mNCe+nhjkohWp24pr7BpUYSbEizYCMAQ6 +LCiBKV2j7QQGy7N1aBmdur17ZepYzR1YV0eI+Kd978aZggsmhjXENQYVTmm/XA== +-----END CERTIFICATE----- diff --git a/bundles/pom.xml b/bundles/pom.xml index 4a2b07e052c..0fff040f034 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -144,6 +144,7 @@ org.openhab.binding.folding org.openhab.binding.foobot org.openhab.binding.freebox + org.openhab.binding.freeboxos org.openhab.binding.fronius org.openhab.binding.fsinternetradio org.openhab.binding.ftpupload