added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.ipcamera/.classpath
Normal file
32
bundles/org.openhab.binding.ipcamera/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry exported="true" kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.ipcamera/.project
Normal file
23
bundles/org.openhab.binding.ipcamera/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.ipcamera</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.ipcamera/NOTICE
Normal file
13
bundles/org.openhab.binding.ipcamera/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
861
bundles/org.openhab.binding.ipcamera/README.md
Normal file
861
bundles/org.openhab.binding.ipcamera/README.md
Normal file
@@ -0,0 +1,861 @@
|
||||
# IP Camera Binding
|
||||
|
||||
This binding allows you to use most IP cameras in openHAB and has many hidden features, so please take the time to read through this guide to learn different ways to work with cameras that you may not know about.
|
||||
I recommend purchasing a brand of camera that has an open API, as many features use far less CPU when done with an API camera.
|
||||
They usually have better picture quality and more advanced features compared to lower priced cameras.
|
||||
|
||||
To see what features each brand has implemented from their APIs, please see this post:
|
||||
|
||||
<https://community.openhab.org/t/ipcamera-new-ip-camera-binding/42771>
|
||||
|
||||
## How to Get Help
|
||||
|
||||
+ Check this readme for any setup steps for your brand.
|
||||
+ Check if the camera is offline, if so there will be a reason listed.
|
||||
+ Always look at the log files with TRACE enabled, as any FFmpeg and camera errors may not reach the INFO logs.
|
||||
To enable TRACE logging, enter this in the openHAB console `log:set TRACE org.openhab.binding.ipcamera`.
|
||||
+ Search the forum using any log messages to find how others have already solved it.
|
||||
+ Only after doing the above ask for help in the forum and create a new thread.
|
||||
|
||||
## Special Notes for Different Brands
|
||||
|
||||
### Generic Cameras
|
||||
|
||||
This binding can turn a RTSP stream into snapshots (.jpg still pictures), create motion and audio alarms, cast the camera and much more.
|
||||
FFmpeg is required to be installed for these features to work and this must be done manually.
|
||||
FFmpeg can be installed very easily see [Video Streams](#video-streams).
|
||||
|
||||
If your camera has a snapshot URL, provide it to the binding via the config called `snapshotUrl` after first testing the URL in any browser.
|
||||
When using FFmpeg instead of a provided URL to create snapshots, it requires more CPU that you can turn on and off via the `pollImage` channel using a switch or rule.
|
||||
|
||||
Snapshots generated by FFmpeg use the key frames (iFrames) to lower the CPU load, and since some cameras only produce a key frame every 2+ seconds with their default settings, this will effect how often a snapshot is produced.
|
||||
Some cameras allow the key frame to be created every second or a different amount by the user, refer to your cameras manual and support on how to do this.
|
||||
|
||||
### ESP32 Cameras
|
||||
|
||||
These cameras do not have the ability to create H.264 streams and hence can not be used with HLS, however all other features should work.
|
||||
See the [Full Example](#full-example) section for how to setup these cameras.
|
||||
Due to many custom firmwares available, you may need to ask the firmware developer what the URLs are for snapshots and MJPEG streams if they have changed the defaults from what the Arduino IDE sample code uses.
|
||||
|
||||
### Amcrest
|
||||
|
||||
It is better to always setup your Amcrest camera as a `dahua` thing type.
|
||||
The old alarm polling based method is used if you setup as `amcrest`, and the newer/better event based method is used if you setup as `dahua` instead.
|
||||
All other features should be the same between the two.
|
||||
|
||||
### Dahua
|
||||
|
||||
For MJPEG to work, you need to set the first sub-stream to use MJPEG format for the default settings to work, otherwise you can override the default with `mjpegUrl` config with a valid URL for MJPEG streams.
|
||||
|
||||
### Foscam
|
||||
|
||||
* If the user/pass is wrong, the camera can lockout and refuse to answer the binding requiring a reboot of the camera, so be sure the details are correct before the camera tries to poll the camera too many times.
|
||||
* To use MJPEG streaming, you need to enable one of the streams to use this format. This can be done by entering this into any browser:`http://ip:88/cgi-bin/CGIProxy.fcgi?cmd=setSubStreamFormat&format=1&usr=admin&pwd=password`
|
||||
* If your camera does not support MJPEG as some Foscams no longer do, then you can set `mjpegUrl` to contain *ffmpeg* to use your CPU to generate a MJPEG stream.
|
||||
* Some Foscam cameras need to have a detection area listed in the URL when you enable the motion alarm.
|
||||
|
||||
As each Foscam model has a different resolution and two different URLs, this makes it difficult to automate, so an override feature was added to create your own "enable the alarm" URL.
|
||||
This setting is called `customMotionAlarmUrl` and the steps to using it are:
|
||||
|
||||
1. Enable the motion alarm in the web interface of your camera and setup any areas you wish movement to be ignored in. E.g. tree branches moving in the wind.
|
||||
2. Use any web browser to fetch this URL `https://x.x.x.x/cgi-bin/CGIProxy.fcgi?cmd=getMotionDetectConfig1&usr=xxxxx&pwd=xxxxx`
|
||||
3. Use the information returned by the above URL to create the override settings.
|
||||
|
||||
An example for a Foscam C2 is...
|
||||
|
||||
```
|
||||
/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig1&isEnable=1&snapInterval=1&schedule0=281474976710655&schedule1=281474976710655&schedule2=281474976710655&schedule3=281474976710655&schedule4=281474976710655&schedule5=281474976710655&schedule6=281474976710655&x1=0&y1=0&width1=10000&height1=10000&sensitivity1=1&valid1=1&linkage=6&usr=xxxxx&pwd=xxxxx
|
||||
```
|
||||
|
||||
Another example is:
|
||||
|
||||
```
|
||||
/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig&isEnable=1&linkage=0001&sensitivity=1&triggerInterval=15&schedule0=281474976710655&schedule1=281474976710655&schedule2=281474976710655&schedule3=281474976710655&schedule4=281474976710655&schedule5=281474976710655&schedule6=281474976710655&area0=1023&area1=1023&area2=1023&area3=1023&area4=1023&area5=1023&area6=1023&area7=1023&area7=1023&area8=1023&area9=1023&usr=username&pwd=password
|
||||
```
|
||||
|
||||
### Hikvision
|
||||
|
||||
+ For MJPEG to work, you need to set the first sub-stream to be in MJPEG format, otherwise you can override the default with the `mjpegUrl` config with a valid URL for MJPEG streams.
|
||||
+ The CGI/API and also ONVIF features are disabled by default on these cameras, so enable and create user details for ONVIF that are the same user/pass as what you have given the binding.
|
||||
If your camera does not have PTZ (Pan Tilt Zoom) then you can leave ONVIF disabled and just enable the CGI/API.
|
||||
+ Each alarm you wish to use must have `Notify Surveillance Center` enabled under each alarms settings in the control panel of the camera itself.
|
||||
|
||||
### Hikvision NVRs
|
||||
|
||||
In case your Hikvision NVR does not communicate with the binding, make sure that:
|
||||
|
||||
* ISAPI is enabled in the NVR settings.
|
||||
* ONVIF is enabled and a user/pass created that match the bindings.
|
||||
* RTSP is enabled.
|
||||
* Some NVR's allow each camera to be exposed on a set port to give direct access to each camera, some users report this works the best and needs to be enabled.
|
||||
|
||||
Some older versions of these NVRs require setting a different snapshot URL (`snapshotUrl`), as well as `ffmpegInput`.
|
||||
The older ones use the same URLs just with 'ISAPI' removed.
|
||||
|
||||
```java
|
||||
Thing ipcamera:hikvision:West "West Camera"
|
||||
[
|
||||
ipAddress="192.168.0.XX",
|
||||
username="username",
|
||||
password="password",
|
||||
onvifPort=8000, //normally 80 check what it needs
|
||||
port=80,
|
||||
nvrChannel=4,
|
||||
serverPort=54324,
|
||||
ffmpegOutput="/etc/openhab2/html/cameras/camera-west/",
|
||||
ffmpegInput="rtsp://192.168.0.XX:554/ISAPI/Streaming/channels/401"
|
||||
]
|
||||
```
|
||||
|
||||
### Instar
|
||||
|
||||
+ For MJPEG to work, you need to set the first sub-stream to be MJPEG format for the default settings to work, otherwise you can override the default with mjpegUrl with a valid URL for MJPEG streams.
|
||||
+ Be sure to update to the latest firmware for your camera as Instar have made a lot of improvements recently, including adding MQTT features (MQTT is not needed for this binding to work).
|
||||
|
||||
## Discovery
|
||||
|
||||
The discovery feature of openHAB can be used to find and setup any ONVIF cameras.
|
||||
This method should be preferred as it will discover the camera, ports and URLs for you, making the setup much easier.
|
||||
To use the discovery, just press the `+` icon located in the Inbox, then select the IpCamera binding from the list of installed bindings.
|
||||
If your camera is not found after a few searches, it may not be ONVIF and in this case you will need to manually add via the UI your camera as a `generic` thing type and provide the URLs manually.
|
||||
|
||||
## Supported Things
|
||||
|
||||
If using openHAB's textual configuration, or when needing to setup HABpanel/sitemaps, you may need to know what your camera is as a "thing type".
|
||||
|
||||
Example: The thing type for a camera with no ONVIF support is "generic".
|
||||
|
||||
| Thing Type ID | Description |
|
||||
|-|-|
|
||||
| `generic` | For any camera that is not ONVIF compatible, yet has working RTSP or HTTP URLs. |
|
||||
| `onvif` | Use for all ONVIF cameras that do not have an API. |
|
||||
| `amcrest` | Only use for if your Amcrest cameras wont work as a `dahua` thing. This uses an older polling based method for alarms that is not as efficient as the newer method used in `dahua`. Amcrest are made by Dahua and hence the API is similar. |
|
||||
| `dahua` | Use for all Dahua and Amcrest cameras that support the API. |
|
||||
| `doorbird` | Use for all current Doorbird cameras as they support an API as well as ONVIF. |
|
||||
| `foscam` | Use for all current Foscam HD cameras as they support an API as well as ONVIF. |
|
||||
| `hikvision` | Use for all current Hikvision cameras as they support an API as well as ONVIF. |
|
||||
| `instar` | Use for all current Instar cameras as they support an API as well as ONVIF. |
|
||||
| `group` | Used to display or cast multiple cameras like they are a single camera. This is an advanced feature that may require some tweaking of the cameras settings to fully work. |
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
After a camera is added, the first step is to provide login details and a valid serverPort for your camera before it will come online.
|
||||
If your camera is not ONVIF/API based, you will also need to provide the binding with the cameras URLs to the relevant config field/s.
|
||||
For ONVIF cameras that auto detect the wrong URL, these same fields can be used to force a URL of your choosing but leaving them blank will allow the binding to find the URL for you.
|
||||
|
||||
NOTE: Leave any `user:pass@` out of any URLs, the binding will handle this for you.
|
||||
Not only does this hide your login details, it will also make changing your password much easier if it is only located in 1 field.
|
||||
|
||||
Below is a list of all configuration parameters (useful for textual config) with a short description.
|
||||
If you do not specify any of these, the binding will use the default which should work in most cases and should be tried first.
|
||||
|
||||
| Parameter | Description |
|
||||
|-|-|
|
||||
| `ipAddress`| The IP address or host name of your camera. |
|
||||
| `port`| This port will be used for HTTP calls for fetching the snapshot and any API calls. |
|
||||
| `onvifPort`| The port your camera uses for ONVIF connections. This is needed for PTZ movement, events, and the auto discovery of RTSP and snapshot URLs. |
|
||||
| `serverPort`| The port that will serve the video streams and snapshots back to openHAB without authentication. You can choose any number, but it must be unique and unused for each camera that you setup. Setting the port to -1 (default), will turn all file serving off and some features will fail to work. |
|
||||
| `username`| Leave blank if your camera does not use login details. |
|
||||
| `password`| Leave blank if your camera does not use login details. |
|
||||
| `onvifMediaProfile`| 0 (default) is your cameras Mainstream and the numbers above 0 are the substreams. Any auto discovered URLs will use the streams that this indicates. You can always override the URLs should you wish to use something different for one of them. |
|
||||
| `pollTime`| Time in milliseconds between fetching a JPG. Note: Most features will not poll non stop and are done "on demand" to keep network traffic to a minimum. The exception is when using the GIF preroll feature, this will cause the camera to always fetch a snapshot every poll. |
|
||||
| `updateImageWhen`| The `Image` channel can be set to update in a number of ways to help reduce network traffic. Recommend that you DO NOT USE the image channel unless you use a very large pollTime. |
|
||||
| | `0` - Default and the RECOMMENDED setting, the Image channel never updates. |
|
||||
| | `1` - Update the Image channel only when the `pollImage` channel is turned on.|
|
||||
| | `2` - Start of Motion Alarms will update the Image channel. |
|
||||
| | `3` - Start of Audio Alarm will update the Image channel. |
|
||||
| | `23` - Start of Motion and Audio Alarms will update the Image channel. |
|
||||
| | `4` - During Motion Alarm the Image channel will update every poll until Alarm stops. |
|
||||
| | `5` - During Audio Alarm the Image channel will update every poll until Alarm stops. |
|
||||
| | `45` - During Motion and Audio Alarms the Image channel will update every poll until both alarms stop. |
|
||||
| `nvrChannel`| Set this to `1` (default) if it is a standalone camera, or to the input channel number of your NVR that the camera is connected to. This effects the hard coded URLs for the API based cameras. |
|
||||
| `snapshotUrl`| Leave this empty to auto detect the snapshot URL if the camera has ONVIF, or enter a HTTP address if you wish to override with a different address. Setting this to `ffmpeg` forces the camera to use FFmpeg to create the snapshots from the RTSP stream. |
|
||||
| `alarmInputUrl` | A URL you can use for the FFmpeg created Audio and Motion Alarms as they don't require high res feeds as they are not seen. |
|
||||
| `customMotionAlarmUrl`| Foscam only, for custom enable motion alarm use. More info found in Foscam's setup steps. |
|
||||
| `customAudioAlarmUrl`| Foscam only, for custom enable audio alarm use. More info found in Foscam's setup steps. |
|
||||
| `mjpegUrl`| A HTTP URL for MJPEG format streams. If you enter 'ffmpeg' the stream can be generated from the RTSP URL. |
|
||||
| `ffmpegInput`| Best if this stream is in H.264 format and can be RTSP or HTTP URLs. Leave this blank to use the auto detected RTSP address for ONVIF cameras. |
|
||||
| `ffmpegInputOptions` | Allows you to specify any options before the -i on the commands for FFmpeg. If you have a ESP32 camera that only has a mjpeg stream then make this equal `-f mjpeg`. |
|
||||
| `ffmpegLocation`| The full path including the filename for where you have installed FFmpeg. The default should work for most Linux installs but if using windows use this format: `c:\ffmpeg\bin\ffmpeg.exe` |
|
||||
| `ffmpegOutput`| The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this format: `c:\openhabconf\html\ipcamera\`. If you would like to expose the GIF files to your static server, you can set it to `/etc/openhab2/html/cameras/camera-name/` |
|
||||
| `hlsOutOptions`| This gives you direct access to specify your own FFmpeg options to be used. Default: `-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 2 -hls_list_size 4` |
|
||||
| `gifOutOptions`| This gives you direct access to specify your own FFmpeg options to be used for animated GIF files. Default: `-r 2 -filter_complex scale=-2:360:flags=lanczos,setpts=0.5*PTS,split[o1][o2];[o1]palettegen[p];[o2]fifo[o3];[o3][p]paletteuse` |
|
||||
| `mjpegOptions` | Allows you to change the settings for creating a MJPEG stream from RTSP using FFmpeg. Possible reasons to change this would be to rotate or re-scale the picture from the camera, change the JPG compression for better quality or the FPS rate. |
|
||||
| `motionOptions` | This gives access to the FFmpeg parameters for detecting motion alarms from a RTSP stream. One possible use for this is to use the CROP feature to ignore any trees that move in the wind or a timecode stamp. Crop will not remove the trees from your picture, it only ignores the movement of the tree. |
|
||||
| `gifPreroll`| Store this many snapshots from BEFORE you trigger a GIF creation. Default: `0` will not use snapshots and will instead use a realtime stream from the ffmpegInput URL |
|
||||
| `gifPostroll`| How long in seconds to create a GIF from a stream. Alternatively if `gifPreroll` is set to value greater than `0`, this is how many snapshots to use AFTER you trigger a GIF creation as snapshots occur at the poll rate. |
|
||||
| `ipWhitelist`| Enter any IPs inside brackets that you wish to allow to access the video stream. `DISABLE` the default value will turn this feature off. Example: `ipWhitelist="(127.0.0.1)(192.168.0.99)"` |
|
||||
| `ptzContinuous`| If set to false (default) the camera will move using Relative commands, If set to true the camera will instead use continuous movements and will require an `OFF` command to stop the movement. |
|
||||
|
||||
## Channels
|
||||
|
||||
Each camera brand will have different channels depending on how much of the support for an API has been added.
|
||||
The channels are kept consistent as much as possible from brand to brand to make upgrading to a different camera easier.
|
||||
|
||||
| Channel | Type | Description |
|
||||
|-|-|-|
|
||||
| `activateAlarmOutput` | Switch | Toggles a cameras relay output 1. |
|
||||
| `activateAlarmOutput2` | Switch | Toggles a cameras relay output 2. |
|
||||
| `audioAlarm` | Switch (read only) | When the camera detects noise above a threshold this switch will move to ON. |
|
||||
| `autoLED` | Switch | When ON this sets a cameras IR LED to automatically turn on or off. |
|
||||
| `cellMotionAlarm` | Switch (read only) | ONVIF cameras only will reflect the status of the ONVIF event of the same name. |
|
||||
| `doorBell` | Switch (read only) | Doorbird only, will reflect the status of the doorbell button. |
|
||||
| `enableAudioAlarm` | Switch | Allows the audio alarm to be turned ON or OFF. |
|
||||
| `enableExternalAlarmInput` | Switch | Hikvision and Instar allow the Alarm input terminals to be disabled by this control. |
|
||||
| `enableFieldDetectionAlarm` | Switch | Allows the field detection alarm to be turned ON or OFF. Some cameras will call this the Intrusion Alarm. |
|
||||
| `enableLED` | Switch | Turn the IR LED ON or OFF. Some cameras have 3 states the LED can be in, so see the `autoLED` channel. |
|
||||
| `enableLineCrossingAlarm` | Switch | Turns the line crossing alarm for API cameras, ON and OFF. |
|
||||
| `enableMotionAlarm` | Switch | Turns the motion alarm ON and OFF for API cameras. This will not effect FFmpeg based alarms which have their own control. |
|
||||
| `enablePirAlarm` | Switch | Turn PIR sensor ON or OFF. |
|
||||
| `externalAlarmInput` | Switch (read only) | Reflects the status of the alarm input terminals on some cameras. |
|
||||
| `externalAlarmInput2` | Switch (read only) | Reflects the status of the alarm input 2 terminals on some cameras. |
|
||||
| `externalLight` | Switch | Some cameras have a dedicated relay output for turning lights on and off with. |
|
||||
| `externalMotion` | Switch | Can be used to inform the camera if it has motion in its view area. Handy if you own a PIR or any other kind of external sensor. If you use the autofps.mjpeg feature, this could increase the frame rate when a door that was closed is opened. Note: It will not be passed onto your camera and will not trigger any recordings. |
|
||||
| `faceDetected` | Switch (read only) | When a camera detects a face (API cameras only) this switch will move to ON. |
|
||||
| `fieldDetectionAlarm` | Switch (read only) | Reflects the cameras status for the field or intrusion alarm. |
|
||||
| `ffmpegMotionAlarm` | Switch (read only) | The status of the FFmpeg based motion alarm. |
|
||||
| `ffmpegMotionControl` | Dimmer | This control allows FFmpeg to detect movement from a RTSP or HTTP source and inform openHAB. The channel that will move is called `ffmpegMotionAlarm`. |
|
||||
| `gifHistory` | String | The 50 most recent filenames the binding has used unless reset. |
|
||||
| `gifHistoryLength` | Number | How many filenames are in the `gifHistory`. |
|
||||
| `gotoPreset` | String | ONVIF cameras that can move only. Will cause the camera to move to a preset location. |
|
||||
| `hlsUrl` | String | The URL for the ipcamera.m3u8 file. |
|
||||
| `imageUrl` | String | The URL for the ipcamera.jpg file. |
|
||||
| `itemLeft` | Switch (read only) | Will turn ON if an API camera detects an item has been left behind. |
|
||||
| `itemTaken` | Switch (read only) | Will turn ON if an API camera detects an item has been stolen. |
|
||||
| `lastMotionType` | String | Cameras with multiple alarm types will update this with which alarm last detected motion, i.e. a lineCrossing, faceDetection or item stolen alarm. You can also use this to create a timestamp of when the last motion was detected by creating a rule when this channel changes. |
|
||||
| `lineCrossingAlarm` | Switch (read only) | Will turn on if the API camera detects motion has crossed a line. |
|
||||
| `mjpegUrl` | String | The URL for the ipcamera.mjpeg stream. |
|
||||
| `motionAlarm` | Switch (read only) | The status of the 'video motion' events in ONVIF and API cameras. Also see `cellMotionAlarm` as these can give different results. |
|
||||
| `mp4History` | String | The 50 most recent filenames the binding has used unless reset. |
|
||||
| `mp4HistoryLength` | Number | How many filenames are in the `mp4History`. Setting this to 0 will clear the history. |
|
||||
| `pan` | Dimmer | Works with ONVIF cameras that can be moved. |
|
||||
| `parkingAlarm` | Switch (read only) | When an API camera detects a car, this will turn ON. |
|
||||
| `pirAlarm` | Switch (read only) | When a camera with PIR ability detects motion, this turns ON. |
|
||||
| `recordingGif` | Number (read only) | How many seconds recording to GIF for. 0 when file ready. |
|
||||
| `recordingMp4` | Number (read only) | How many seconds recording to MP4 for. 0 when file ready. |
|
||||
| `rtspUrl` | String | The URL for the cameras auto detected RTSP stream. |
|
||||
| `sceneChangeAlarm` | Switch (read only) | When an API camera detects the camera has moved, this turns ON. |
|
||||
| `startStream` | Switch | Starts the HLS files being created, if it not manually moved it will indicate if the files are being created on demand. |
|
||||
| `storageAlarm` | Switch (read only) | When an ONVIF cameras storage is full and/or removed, this turns ON. |
|
||||
| `tamperAlarm` | Switch (read only) | When an ONVIF cameras tamper switch is tripped, this turns ON. |
|
||||
| `textOverlay` | String | Dahua, Instar and Hikvision can overlay any text you enter here over the video stream. |
|
||||
| `thresholdAudioAlarm` | Dimmer | This channel can be linked to a Switch and a Slider. The value of the slider is the value in dB that is detected as noise/alarm down from digital full scale. Higher values are more sensitive and will trigger the alarm with quieter / less noise. |
|
||||
| `tilt` | Dimmer | Works with ONVIF cameras that can be moved. |
|
||||
| `triggerExternalAlarmInput` | Switch | Hikvision cameras can change if the alarm input terminal is ON when high or low. This can be used to manually cause an alarm input event to occur. |
|
||||
| `tooBlurryAlarm` | Switch (read only) | ONVIF cameras only will reflect the status of the ONVIF event of the same name. |
|
||||
| `tooBrightAlarm` | Switch (read only) | ONVIF cameras only will reflect the status of the ONVIF event of the same name. |
|
||||
| `tooDarkAlarm` | Switch (read only) | ONVIF cameras only will reflect the status of the ONVIF event of the same name. |
|
||||
| `pollImage` | Switch | This control can be used to manually start and stop using your CPU to create snapshots from a RTSP source. If you have a snapshot URL setup in the binding, only then can this control can be used to update the Image channel. |
|
||||
| `zoom` | Dimmer | Works with ONVIF cameras that can be moved. |
|
||||
|
||||
## Moving PTZ Cameras
|
||||
|
||||
To move a camera with this binding you need an ONVIF camera that supports one of the following:
|
||||
|
||||
+ Absolute movements
|
||||
+ Relative movements
|
||||
+ Continuous movements
|
||||
+ Presets
|
||||
|
||||
To test your cameras compatibility and also to create some preset locations, use a free program called `ONVIF Device Manager` (ODM for short).
|
||||
Not all ONVIF cameras work with all of the methods, so testing first to confirm what works is a good idea and the presets can not be created with the binding, only loaded after they are already created in a program like ODM.
|
||||
After creating new or changing existing presets, it is necessary to send the REFRESH command to the `gotoPreset` channel or you can restart the binding if that is easier.
|
||||
You can create names using the mappings feature of the selection element.
|
||||
See docs here <https://www.openhab.org/docs/configuration/sitemaps.html#mappings>
|
||||
|
||||
Moving the camera using *Relative* or *Continuous* (config `ptzContinuous` must be true) movements can be done by sending the INCREASE and DECREASE commands to the Pan, Tilt and Zoom channels.
|
||||
The OFF command (to any of the PTZ channels) will stop the cameras movements in the case of continuous being selected.
|
||||
When the config is set to false (the default if not specified) the binding will send relative movements.
|
||||
There are some widgets created in the HABpanel widget gallery that you can download and use right away saving you time if your camera supports either presets, relative or continuous modes.
|
||||
|
||||
For sitemaps, the below examples can be used.
|
||||
|
||||
|
||||
item:
|
||||
|
||||
```java
|
||||
|
||||
String BabyCamGotoPreset "Goto Preset" { channel="ipcamera:dahua:BabyCamera:gotoPreset" }
|
||||
Dimmer BabyCamPan "Pan [%d] left/right" { channel="ipcamera:dahua:BabyCamera:pan" }
|
||||
Dimmer BabyCamTilt "Tilt [%d] up/down" { channel="ipcamera:dahua:BabyCamera:tilt" }
|
||||
Dimmer BabyCamZoom "Zoom [%d] in/out" { channel="ipcamera:dahua:BabyCamera:zoom" }
|
||||
|
||||
```
|
||||
|
||||
sitemap:
|
||||
|
||||
```java
|
||||
|
||||
Selection item=BabyCamGotoPreset
|
||||
Switch item=BabyCamPan mappings=[INCREASE="Left",OFF="STOP",DECREASE="Right"]
|
||||
Switch item=BabyCamTilt mappings=[INCREASE="Down",OFF="STOP",DECREASE="Up"]
|
||||
Switch item=BabyCamZoom mappings=[INCREASE="Zoom In",OFF="STOP",DECREASE="Zoom Out"]
|
||||
Slider item=BabyCamPan icon=movecontrol
|
||||
Slider item=BabyCamTilt icon=movecontrol
|
||||
Slider item=BabyCamZoom icon=zoom
|
||||
|
||||
```
|
||||
|
||||
Moving the camera to an EXACT repeatable location (Preset 1 saved location) with a rule:
|
||||
|
||||
```java
|
||||
BabyCamGotoPreset.sendCommand(1)
|
||||
```
|
||||
|
||||
Moving the camera to an EXACT repeatable location using Absolute movement with a rule:
|
||||
|
||||
```java
|
||||
BabyCamPan.sendCommand(22)
|
||||
BabyCamTilt.sendCommand(60)
|
||||
BabyCamZoom.sendCommand(0)
|
||||
```
|
||||
|
||||
## FFmpeg Motion and Audio Alarms
|
||||
|
||||
Any camera can use FFmpeg to create either a `ffmpegMotionAlarm` or `audioAlarm`.
|
||||
Before using this feature, consider if the <https://www.openhab.org/addons/bindings/ftpupload/> is better used if your camera can upload to a FTP (binding can creates a virtual FTP so you don't need one).
|
||||
A Zwave or Zigbee PIR sensor is another way to save the CPU load this feature creates.
|
||||
|
||||
Even if your camera has a motion alarm, you may find that it does not provide enough flexibility to ignore moving trees, or have its sensitivity adjusted on the fly to reduce its sensitivity during rain.
|
||||
This is where this feature can come in handy, as you can add any of the many FFmpeg filters like crop as this wont effect the video feeds you watch.
|
||||
<https://ffmpeg.org/ffmpeg-filters.html#Examples-52>
|
||||
|
||||
To get this working:
|
||||
|
||||
+ Provide a URL to the bindings config `alarmInputUrl` or leave it blank to use the auto detected URL if your camera has ONVIF.
|
||||
+ Install FFmpeg.
|
||||
+ You have the resolution and FPS at realistic settings for your CPU. You need to reach 1.x speed otherwise the alarm will lag further behind realtime the longer you have this running.
|
||||
1080p and 10 fps maximum for an ARM processor is probably a good place to start testing or even lower if you can.
|
||||
+ Set the `ffmpegMotionControl` channel to 16 with a slider control and if the alarm stays on increase the value until it works as desired.
|
||||
If it will not trigger, lower the control until it does.
|
||||
+ Set the `ffmpegMotionControl` to OFF or 0 and it stops using your CPU.
|
||||
You can link this same channel to BOTH a switch and a slider at the same time if you like to have both types of controls.
|
||||
+ The output of the alarm will go to a channel called `ffmpegMotionAlarm` and you can use the `lastMotionType` channel to determine which alarm was last tripped if your camera has multiple alarm types.
|
||||
|
||||
**audioAlarm**
|
||||
|
||||
This works in the same way, just with different channels.
|
||||
If you setup a lower resolution URL in the config `alarmInputUrl` you need to ensure it contains audio otherwise this feature wont work.
|
||||
A value of 10 on a slider translates to -10dB below max volume (digital full scale) and when the audio goes above the -10dB threshold the alarm will turn ON.
|
||||
|
||||
## Image / Snapshots
|
||||
|
||||
There are a number of ways to use snapshots with this binding.
|
||||
|
||||
**Ways to use snapshots are:**
|
||||
|
||||
+ Use the cameras URL so it passes from the camera directly to your end device. ie a tablet.
|
||||
This is always the best option if it works.
|
||||
+ Request a snapshot with the URL `http://192.168.xxx.xxx:54321/ipcamera.jpg`.
|
||||
The IP is for your openHAB server not the camera, and 54321 is the `serverPort` number that you specified in the bindings setup. If you find the snapshot is old, you can set the `gifPreroll` to a number above 0 and this forces the camera to keep updating the stored JPG in RAM.
|
||||
The ipcamera.jpg can also be cast, as most cameras can not directly cast their snapshots.
|
||||
+ Use the `http://192.168.xxx.xxx:54321/snapshots.mjpeg` to request a stream of snapshots to be delivered in MJPEG format.
|
||||
See the streaming section for more info.
|
||||
+ Use the `updateGif` feature and use a `gifPreroll` value > 0.
|
||||
This creates a number of snapshots in the FFmpeg output folder called snapshotXXX.jpg where XXX starts at 0 and increases each `pollTime`.
|
||||
This allows you to get a snapshot from an exact amount of time before, on, or after triggering the GIF to be created.
|
||||
Handy for cameras which lag due to slow processors, or if you do not want a hand blocking the image when the door bell was pushed.
|
||||
These snapshots can be fetched either directly as they exist on disk, or via this URL format.
|
||||
`http://192.168.xxx.xxx:54321/snapshot0.jpg`
|
||||
+ Also worth a mention is that you can off load cameras to a software package running on a separate server such as, Motion, Shinobi and Zoneminder.
|
||||
|
||||
See this forum thread for examples of how to use snapshots and streams in a sitemap.
|
||||
<https://community.openhab.org/t/ip-camera-how-to-clickable-thumbnail-overview-in-sitemaps-that-opens-up-to-a-larger-view/77990>
|
||||
|
||||
## Video Streams
|
||||
|
||||
To get some of the video formats working, you need to install FFmpeg.
|
||||
Visit their site here to learn how <https://ffmpeg.org/>
|
||||
|
||||
Under Linux, FFmpeg can be installed very easily with this one command.
|
||||
|
||||
```
|
||||
sudo apt update && sudo apt install ffmpeg
|
||||
```
|
||||
|
||||
**IMPORTANT:**
|
||||
The binding has its own file server that works by allowing access to the snapshot and video streams with no user/password for requests that come from an IP located in the `ipWhitelist`.
|
||||
Requests from external IPs or internal requests that are not on the `ipWhitelist` will fail to get any answer.
|
||||
If you prefer to use your own firewall instead, you can also choose to make the `ipWhitelist` equal "DISABLE" (the default since the feature also needs a valid serverPort set) to turn this feature off and then all internal IPs will have access.
|
||||
|
||||
There are multiple ways to get a moving picture, to use them just enter the URL into any browser using `http://192.168.xxx.xxx:serverPort/name.format` replacing the name.format with one of the options that are listed below:
|
||||
|
||||
+ **ipcamera.m3u8** HLS (HTTP Live Streaming) which uses H.264 compression.
|
||||
This can be used to cast to Chromecast devices, or can display video in many browsers (some browsers require a plugin to be installed).
|
||||
Please understand that this format due to the way it works will give you lag behind realtime, more on this below.
|
||||
+ **ipcamera.mjpeg** whilst needing more bandwidth, it is far more compatible for displaying in a wider range of UIs and browsers.
|
||||
It is normally 1 second or less behind real-time.
|
||||
FFmpeg can be used to create this stream if your camera does not create one for you, but this uses more CPU.
|
||||
A lot of cameras limit the resolution in this format, so consider using HLS, autofps.mjpeg, or snapshots.mjpeg instead which will be in a higher resolution.
|
||||
+ **snapshots.mjpeg** is a special MJPEG stream created from the cameras snapshots that are taken at the polling rate.
|
||||
+ **autofps.mjpeg** This requires a camera that has a motion alarm to be turned on or it will only send a picture every 8 seconds.
|
||||
You can also use the `externalMotion` channel to change the framerate.
|
||||
This feature is designed to keep data traffic to your mobile devices as low as possible by automatically sending 1fps when motion is occurring, but only 1 picture every 8 seconds when the picture has no motion.
|
||||
The idea is to not send lots of pictures if the picture has not changed as doing so only eats up your data plan.
|
||||
+ **ipcamera.gif** This is small in size and very compatible and handy to use in push notifications, Pushover, Telegram, or emails.
|
||||
You can cast it which can be handy to show a moving picture that keeps repeating on a Google/Nest home hub or your wall mounted tablet.
|
||||
+ MP4 recordings can be created by the binding and FFmpeg, more on this below.
|
||||
|
||||
## MJPEG Streams
|
||||
|
||||
Cameras that have built in MJPEG abilities can stream to openHAB with the MJPEG format with next to no CPU load, less than 1 second lag, and FFmpeg does not need to be installed.
|
||||
Cameras without this ability can still use this binding to convert their RTSP H.264 format to MJPEG (keep reading for more on this below) and this will take a lot of CPU power to handle the conversion.
|
||||
The alternative is to use HLS format which does not need the conversion and does not use any CPU to speak of.
|
||||
For video without a delay, you need MJPEG and without a camera that can create it, you will need to use a lot of CPU power.
|
||||
This can be done in a dedicated video server which will be the only way with lots of cameras, unless you purchase cameras that have the ability built in.
|
||||
|
||||
An alternative way to keep the CPU load low is to use the `snapshots.mjpeg` feature of the binding to create a stream from the cameras snapshots instead of the RTSP stream.
|
||||
|
||||
The main cameras that can do MJPEG with very low CPU load are Amcrest, Dahua, Hikvision, Foscam HD and Instar HD.
|
||||
To set this up, see [Special Notes for Different Brands](#special-notes-for-different-brands).
|
||||
The binding can then distribute this stream to many devices around your home whilst the camera only sees a single open stream.
|
||||
|
||||
To request the MJPEG stream from the binding, all you need to do is use this link changing the IP to that of your openHAB server and the serverPort to match the settings in the bindings setup for that camera.
|
||||
|
||||
<http://openHABIP:serverPort/ipcamera.mjpeg>
|
||||
|
||||
**Creating MJPEG with FFmpeg**
|
||||
|
||||
To use this feature, all you need to do is set the config `mjpegUrl` to contain "ffmpeg" to use your CPU to generate the MJPEG stream with FFmpeg.
|
||||
For cameras that have an API you can opt to not use the cameras stream and use FFmpeg instead should you run out of available streams.
|
||||
|
||||
FFmpeg may require you to lower the resolution and/or the FPS to lower the CPU load down enough to run, you may need to experiment.
|
||||
To change the settings used by this feature the binding exposes the config `mjpegOptions` which the default is currently `-q:v 5 -r 2 -vf scale=640:-2 -update 1` where 5 is the JPG quality/compression setting, and -r 2 is how many frames per second to try and create.
|
||||
In this case it will create 2 frames every second.
|
||||
`-vf scale=640:-2` will lower the resolution down to make the video 640 pixels wide.
|
||||
You can remove this to use the same resolution as the camera is set to use, however it may become a trade off and you may get less frames per second if you raise the resolution.
|
||||
Always try to get the default settings working first before you begin to experiment and if your stream is above 1080p and 10 frames per second, consider lowering it if you have issues on an ARM based server like a Raspberry PIx.
|
||||
|
||||
## snapshots.mjpeg and autofps.mjpeg
|
||||
|
||||
These similar features allow you to request a MJPEG stream created by the binding with low CPU usage from the cameras snapshots.
|
||||
Snapshots are usually high resolution and look great, however they are limited to a max of 1 frame per second (1 FPS).
|
||||
The reason this is more useful than snapshots on their own, is some UIs will flash white or black when a snapshot is refreshing, this does not happen with snapshots.mjpeg and is the same bandwidth and CPU load as just using snapshots!
|
||||
|
||||
The autofps.mjpeg feature will display a snapshot that updates every 8 seconds to keep network traffic low, then when motion is detected it will automatically increase the frames to every second until the motion stops.
|
||||
This means lower traffic unless the picture is actually changing.
|
||||
|
||||
Request the stream to be sent to an item with this URL.
|
||||
NOTE: The IP is openHAB's not your cameras IP and the 54321 is what you have set as the serverPort.
|
||||
|
||||
`http://192.168.xxx.xxx:54321/snapshots.mjpeg`
|
||||
|
||||
Use the following to display it in your sitemap.
|
||||
|
||||
```
|
||||
Video url="http://192.168.0.32:54321/autofps.mjpeg" encoding="mjpeg"
|
||||
|
||||
Video url="http://192.168.0.32:54321/snapshots.mjpeg" encoding="mjpeg"
|
||||
```
|
||||
|
||||
## HLS (HTTP Live Streaming)
|
||||
|
||||
HLS is a way of splitting the live stream up into small H.264 based files so it can be played in many browsers (some require addons to be installed) without using much CPU power as cameras generally are already in H.264 and this does not transcode the data.
|
||||
Because the files need to be created, this creates a lag/delay behind real time that can be reduced (more on that below).
|
||||
|
||||
The channel called 'startStream' can be used to create HLS files non stop and will remove the startup delay that comes with using this type of stream.
|
||||
The startup delay and the lag are two different things, with the startup delay easily solved by turning this channel ON.
|
||||
If the channel is OFF, the stream will start and stop automatically as required and the channel will reflect its current status.
|
||||
With a fast openHAB server it should only need to be requested once, but on slower ARM systems it takes a while for FFmpeg to get up and running at full speed.
|
||||
|
||||
It can be helpful sometimes to use this line in a rule to start the stream before it is needed further on in the rule `sendHttpGetRequest("http://192.168.0.2:54321/ipcamera.m3u8")` as the stream will stay running for 64 seconds.
|
||||
This 64 second delay before the stream is stopped helps when you are moving back and forth in a UI, as the stream does not keep stopping and needing to start each time you move around in a UI.
|
||||
|
||||
To use the HLS feature, you need to:
|
||||
|
||||
+ Ensure FFmpeg is installed.
|
||||
+ For `generic` cameras, you will need to use the config `ffmpegInput` to provide a HTTP or RTSP URL.
|
||||
+ Supply a folder that the openhab user has write permissions for to the config `ffmpegOutput`.
|
||||
+ Set a valid `serverPort` as the default value of -1 will turn this feature off.
|
||||
+ Consider using a SSD/HDD or a tmpfs (ram drive) can be used if you only have micro SD/flash based storage.
|
||||
|
||||
### Ram Drive Setup
|
||||
|
||||
To create a tmpfs of 20mb at /tmpfs/ run this command to open the file for editing.
|
||||
Recommend using 20Mb per camera that uses this location although it could use less than half that amount if carefully streamlined for less ram.
|
||||
If using the FFmpeg `-hls_wrap wrap` option (causes issues for my Home Hub), you can get away with 5Mb per camera.
|
||||
|
||||
```
|
||||
nano /etc/fstab
|
||||
```
|
||||
|
||||
Enter and save this at the bottom of the file using ctrl X when done.
|
||||
|
||||
```
|
||||
tmpfs /tmpfs tmpfs defaults,nosuid,nodev,noatime,size=20m 0 0
|
||||
```
|
||||
|
||||
### FFmpeg HLS Settings
|
||||
|
||||
Please get the default settings working first before playing with the advanced settings.
|
||||
|
||||
To get audio working you need to have the camera include audio in the stream and in a format that is supported by Chromecast or your browser.
|
||||
Only `AAC` is supported by Google/Nest if you are wanting to cast the stream with audio included.
|
||||
Then you need to change the HLS settings to what you need, some are suggestions are below.
|
||||
|
||||
Less delay behind realtime (no audio) if your cameras iFrames are 1 second apart (-hls_time 1):
|
||||
|
||||
```bash
|
||||
-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 1 -hls_list_size 4
|
||||
```
|
||||
|
||||
For cameras with no audio in the stream (default setting).
|
||||
|
||||
```bash
|
||||
-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 2 -hls_list_size 4
|
||||
```
|
||||
|
||||
For cameras with audio in the stream.
|
||||
Note: will break Chromecast if the camera does not send audio which is why this is not the default.
|
||||
|
||||
```bash
|
||||
-strict -2 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 2 -hls_list_size 4
|
||||
```
|
||||
|
||||
Some browsers require larger segment sizes to prevent choppy playback, this can be done with this setting to create 10 second segment files which increases the time before you can get playback working.
|
||||
|
||||
```bash
|
||||
-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 10 -hls_list_size 4
|
||||
|
||||
```
|
||||
|
||||
### HLS Sitemap Examples
|
||||
|
||||
The webview version allows you to zoom in on the video when using the iOS app, the Video element version does not zoom, but it will pass through myopenHAB.
|
||||
|
||||
```
|
||||
|
||||
Text label="HLS Video Stream" icon="camera"{Video url="http://192.168.1.9:54321/ipcamera.m3u8" encoding="hls"}
|
||||
|
||||
Text label="HLS Webview Stream" icon="camera"{Webview url="http://192.168.1.9:54321/ipcamera.m3u8" height=15}
|
||||
|
||||
```
|
||||
|
||||
**Display multiple HLS streams side by side**
|
||||
|
||||
In order to display camera hls streams side by side you can also create a webView item and link it to a HTML file in the conf/html directory as follows:
|
||||
The webView URL is that of your openHAB installation.
|
||||
|
||||
```
|
||||
Webview url="http://192.168.6.4:8080/static/html/file.html" height=5
|
||||
|
||||
```
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div style="width: 50%; float: left;">
|
||||
<video playsinline autoplay muted controls style="width:100%; " src="http://192.168.6.4:50001/ipcamera.m3u8" />
|
||||
</div>
|
||||
<div style="width: 50%; float: left;">
|
||||
<video playsinline autoplay muted controls style="width: 100%; " src="http://192.168.6.4:50002/ipcamera.m3u8" />
|
||||
</div>
|
||||
<div style="width: 50%; float: left;">
|
||||
<video playsinline autoplay muted controls style="width:100%; " src="http://192.168.6.4:50003/ipcamera.m3u8" />
|
||||
</div>
|
||||
<div style="width: 50%; float: left;">
|
||||
<video playsinline autoplay muted controls style="width: 100%; " src="http://192.168.6.4:50004/ipcamera.m3u8" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
## How to Cast a Camera
|
||||
|
||||
There are two ways to cast a camera.
|
||||
|
||||
1. openHAB Cloud Connector and using metadata/tags.
|
||||
2. Chromecast Bindings `playuri` channel.
|
||||
|
||||
The first method once setup allows you to ask "OK Google show X camera", or "OK Google show X camera on Y display".
|
||||
By optionally naming the display that you wish to use, it can be cast directly to your Chromecast (connected to your TV) by speaking to a Google Nest Mini.
|
||||
This must use the HLS format and use the metadata tag shown below with the openHAB Cloud Connector setup.
|
||||
Don't forget to ask Google to 'sync my devices' after adding the metadata.
|
||||
The synonyms in the tag allows Google to understand multiple names that the camera may be called by different people in your family.
|
||||
|
||||
Example of how this is done in an items file.
|
||||
|
||||
```
|
||||
String FrontDoorCamHlsUrl "Front Door" { channel="ipcamera:ONVIF:FrontDoor:hlsUrl", synonyms="door bell, front camera", ga="Camera" [ protocols="hls" ] }
|
||||
|
||||
```
|
||||
|
||||
The second method is by using the Chromecast Binding and by sending the URL you wish to cast to the `playuri` channel.
|
||||
You can cast the ipcamera.jpg (static picture), ipcamera.gif (looping moving picture), and ipcamera.m3u8 (aka HLS) for a non stop stream that uses low CPU and can also contain audio.
|
||||
|
||||
Example:
|
||||
|
||||
items
|
||||
|
||||
```
|
||||
String KitchenHomeHubPlayURI { channel="chromecast:chromecast:KitchenHomeHub:playuri" }
|
||||
|
||||
```
|
||||
|
||||
In a rule...
|
||||
|
||||
```
|
||||
KitchenHomeHubPlayURI.sendCommand("http://192.168.1.2:54321/ipcamera.m3u8")
|
||||
|
||||
```
|
||||
|
||||
## MP4 and GIF Recordings
|
||||
|
||||
You can use FFmpeg to recording to either GIF or MP4 format.
|
||||
|
||||
The steps to do this are:
|
||||
|
||||
+ Use the Action called `recordMP4(String filename, int secondsToRecord)` or `recordGIF(String filename, int secondsToRecord)` with the first argument being the filename you wish to use, and the second the time in seconds you wish to record for.
|
||||
+ Once the file is created, the channel `recordingMp4` or `recordingGif` will change itself back to `0`, which can be used to trigger a rule to send/use the file which will appear in the `ffmpegOutput` folder.
|
||||
+ The channel `mp4History` or `gifHistory` keeps a string of the last 50 filenames (comma separated values CSV) until you reset the history. If you use `ipcamera` as the filename, this stops the history from growing.
|
||||
+ The channel `mp4HistoryLength` and `gifHistoryLength` keeps track of how many recordings were made since it was last reset.
|
||||
You can send the `0` command to this channel to clear the `mp4History` at the same time as setting this channel back to 0.
|
||||
+ You can use the `mp4OutOptions` or `gifOutOptions` config's to apply any FFmpeg filters that you wish.
|
||||
|
||||
There is also a HABpanel Widget worth checking out that uses the history feature to display a list of recent recordings.
|
||||
<https://community.openhab.org/t/custom-widget-camera-history-and-live-popup/103082>
|
||||
|
||||
**NOTE:** If you are using a tmpfs folder, you will need to ensure you do not run out of space by moving the files with a rule.
|
||||
|
||||
**GIF Preroll**
|
||||
|
||||
There is also a config called `gifPreroll` to be aware of.
|
||||
When `gifPreroll` is 0 (the default) the binding will use the `ffmpegInput` stream to record from.
|
||||
By changing the `gifPreroll` to a value above 0, the binding will change to using snapshots as the source, preventing the need to have or open a RTSP stream.
|
||||
The time between the snapshots then becomes the `pollTime` of the camera (1 second by default) and can be raised if you desire.
|
||||
The snapshots are saved to disk and can be used as a feature that is described in the snapshot section.
|
||||
|
||||
You can request the GIF and MP4 by using this URL format, or by the direct path to where the file is stored:
|
||||
|
||||
<http://openHAB.IP:serverPort/ipcamera.gif>
|
||||
|
||||
.items
|
||||
|
||||
```java
|
||||
|
||||
Number Doorbell_recordingGif "Update GIF" { channel="ipcamera:dahua:DoorCam:recordingGif" }
|
||||
|
||||
```
|
||||
|
||||
.rules
|
||||
|
||||
```java
|
||||
rule "Create front door camera GIF when front doorbell button pushed"
|
||||
when
|
||||
Item FrontDoorbellButton changed to ON
|
||||
then
|
||||
//Start creating the GIF
|
||||
getActions("ipcamera", "ipcamera:dahua:DoorCam").recordGIF("ipcamera",5)
|
||||
//Cast a doorbell sound using the Chromecast binding.
|
||||
KitchenHomeHubPlayURI.sendCommand("http://192.168.1.8:8080/static/doorbell.mp3")
|
||||
end
|
||||
|
||||
rule "Send doorbell GIF via Pushover"
|
||||
when
|
||||
Item Doorbell_recordingGif changed to 0
|
||||
then
|
||||
sendPushoverMessage(pushoverBuilder("Sending GIF from backyard").withApiKey("dsfhghj6546fghfg").withUser("qwerty54657").withDevice("Phone1").withAttachment("/tmpfs/DoorCam/ipcamera.gif"))
|
||||
end
|
||||
```
|
||||
|
||||
## HABpanel
|
||||
|
||||
This section is about how to get things working in HABpanel.
|
||||
|
||||
I highly recommend you check out the easy to use WIDGETS of which there are now 3 that are discussed on the forum here.
|
||||
<https://community.openhab.org/t/custom-widget-camera-clickable-thumbnails-that-open-a-stream/101275>
|
||||
|
||||
The widgets in the link above are the easiest way to get an advanced stream working in openHAB and you are welcome to open them up, look at how they work and change them to something even better that suits your needs.
|
||||
|
||||
## Group Displays
|
||||
|
||||
The [Full Example](#full-example) section shows how to setup a group of cameras to be displayed like they are a single camera.
|
||||
|
||||
Some additional things to check to get it working are:
|
||||
|
||||
+ If using the groups HLS feature, the poll time of the group must be the same or less than the total time contained in each cameras m3u8 file.
|
||||
If you have 3 seconds worth of video segments in each cameras HLS stream, this is the max you can set the poll time of the group to.
|
||||
+ All cameras in a group should have the same HLS segment size setting, 1 and 2 second long segments have been tested to work.
|
||||
|
||||
This is still a very new feature and if you have any issues, please send some TRACE level log output of when the problem occurs.
|
||||
|
||||
## Full Example
|
||||
|
||||
Use the following examples to base your setup on to save some time if you wish to use textual config.
|
||||
Textual config should only be used by advanced users as the [Discovery](#discovery) method should be preferred by new users.
|
||||
|
||||
You can do a find and replace on the thing type from `dahua`, as all cameras use consistent naming of channels and configs so changing between them is easy.
|
||||
|
||||
In the examples you will see the format is: `bindingID:thingType:UID` [param1="string",param2=x,param3=x]
|
||||
|
||||
bindingID: is always ipcamera.
|
||||
thingType: is found listed above under [Supported Things](#supported-things).
|
||||
UID: Can be made up but it must be UNIQUE, hence why it is called uniqueID.
|
||||
|
||||
openHAB's discovery method will use the IP address with the dots removed as the UID.
|
||||
By using textual config, you can name it something useful like "DrivewayCamera" if you wish, or stick with the same convention.
|
||||
|
||||
*.things
|
||||
|
||||
```java
|
||||
|
||||
Thing ipcamera:group:OutsideCameras
|
||||
[
|
||||
pollTime=2000, serverPort=54320,
|
||||
ffmpegOutput="/tmpfs/OutsideGroup/",
|
||||
firstCamera="001",
|
||||
secondCamera="002",
|
||||
thirdCamera="TestCam",
|
||||
forthCamera="",
|
||||
motionChangesOrder=true
|
||||
]
|
||||
|
||||
Thing ipcamera:dahua:001
|
||||
[
|
||||
ipAddress="192.168.0.5", password="suitcase123456",
|
||||
username="admin",
|
||||
serverPort=54321,
|
||||
ffmpegOutput="/tmpfs/camera1/"
|
||||
]
|
||||
|
||||
Thing ipcamera:hikvision:002
|
||||
[
|
||||
ipAddress="192.168.0.6", password="suitcase123456",
|
||||
username="admin",
|
||||
serverPort=54322,
|
||||
ffmpegOutput="/tmpfs/camera2/"
|
||||
]
|
||||
|
||||
Thing ipcamera:generic:TestCam
|
||||
[
|
||||
ipAddress="192.168.0.7", password="pass123", username="admin", serverPort=54323,
|
||||
snapshotUrl="http://192.168.1.65/tmpfs/snap.jpg", //remove this line if your camera has none
|
||||
mjpegUrl="ffmpeg",
|
||||
ffmpegOutput="/tmpfs/HttpTest/",
|
||||
ffmpegInput="rtsp://192.168.1.65:554/11"
|
||||
]
|
||||
|
||||
Thing ipcamera:generic:TTGoCamera "ESP32 TTGo Camera" @ "Cameras"
|
||||
[
|
||||
ipAddress="192.168.1.181",
|
||||
serverPort=51321,
|
||||
port=80,
|
||||
gifPreroll=1,
|
||||
gifPostroll=6,
|
||||
snapshotUrl="http://192.168.1.181/capture",
|
||||
mjpegUrl="http://192.168.1.181:81/stream",
|
||||
ffmpegOutput="/tmpfs/TTGoCamera/",
|
||||
ffmpegInput="http://192.168.1.181:81/stream",
|
||||
ffmpegInputOptions="-f mjpeg",
|
||||
ipWhitelist="(192.168.2.8)(192.168.2.83)(192.168.2.99)"
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
*.items
|
||||
|
||||
```java
|
||||
|
||||
Number BabyCamDirection "Camera Direction"
|
||||
Switch BabyCamEnableMotion "MotionAlarm on/off" { channel="ipcamera:dahua:001:enableMotionAlarm" }
|
||||
Switch BabyCamMotionAlarm "Motion detected" { channel="ipcamera:dahua:001:motionAlarm" }
|
||||
Switch BabyCamEnableAudioAlarm "AudioAlarm on/off" { channel="ipcamera:dahua:001:enableAudioAlarm" }
|
||||
Switch BabyCamAudioAlarm "Audio detected" { channel="ipcamera:dahua:001:audioAlarm" }
|
||||
Dimmer BabyCamAudioThreshold "Audio Threshold [%d]" { channel="ipcamera:dahua:001:thresholdAudioAlarm" }
|
||||
Dimmer BabyCamLED "IR LED [%d]" { channel="ipcamera:dahua:001:enableLED" }
|
||||
Switch BabyCamAutoLED "Auto IR LED" { channel="ipcamera:dahua:001:autoLED" }
|
||||
String BabyCamTextOverlay "Text to overlay" { channel="ipcamera:dahua:001:textOverlay" }
|
||||
String BabyCamHlsUrl "HLS Stream" { channel="ipcamera:dahua:BabyCamera:hlsUrl" }
|
||||
DateTime BabyCamLastMotionTime "Time motion was last detected [%1$ta %1$tR]"
|
||||
String BabyCamLastMotionType "Last Motion Type" { channel="ipcamera:dahua:BabyCamera:lastMotionType" }
|
||||
Switch BabyCamStartHLS { channel="ipcamera:dahua:BabyCamera:startStream" }
|
||||
|
||||
Dimmer GenericMotionControl "Motion Threshold [%d]" { channel="ipcamera:generic:TestCam:ffmpegMotionControl" }
|
||||
Switch GenericMotionAlarm "Motion detected" { channel="ipcamera:generic:TestCam:motionAlarm" }
|
||||
Dimmer GenericAudioThreshold "Audio Threshold [%d]" { channel="ipcamera:generic:TestCam:thresholdAudioAlarm" }
|
||||
Switch GenericAudioAlarm "Audio detected" { channel="ipcamera:generic:TestCam:audioAlarm" }
|
||||
|
||||
String OutsideCameraGroupHlsUrl "Outside Cameras" { channel="ipcamera:group:OutsideCameras:hlsUrl", ga="Camera" [ protocols="hls" ] }
|
||||
Switch OutsideCameraGroupStartHLS "Start outside HLS" { channel="ipcamera:group:OutsideCameras:startStream" }
|
||||
|
||||
```
|
||||
|
||||
*.sitemap
|
||||
|
||||
```java
|
||||
Text label="Outside Camera Group" icon="camera"{Image url="http://192.168.0.2:54320/ipcamera.jpg" refresh=1000}
|
||||
|
||||
Text label="BabyMonitor" icon="camera"{
|
||||
Default item=BabyCamMotionAlarm icon=siren
|
||||
Default item=BabyCamAudioAlarm icon=siren
|
||||
Text label="Advanced Controls" icon="settings"{
|
||||
Switch item=BabyCamEnableMotion
|
||||
Default item=BabyCamEnableAudioAlarm
|
||||
Default item=BabyCamAudioThreshold icon=recorder
|
||||
Slider item=BabyCamLED
|
||||
Default item=BabyCamAutoLED
|
||||
}
|
||||
Text label="Last Movement" icon="motion"{
|
||||
Webview url="http://192.168.0.2:54321/ipcamera.gif" height=9
|
||||
Default item=BabyCamMotionAlarm icon=siren
|
||||
Default item=BabyCamLastMotionTime
|
||||
Default item=BabyCamLastMotionType
|
||||
}
|
||||
Text label="Cameras MJPEG Stream" icon="camera"{Video url="http://192.168.0.2:54321/ipcamera.mjpeg" encoding="mjpeg"}
|
||||
Text label="Snapshot 1FPS Stream" icon="camera"{Video url="http://192.168.0.2:54321/snapshots.mjpeg" encoding="mjpeg"}
|
||||
Text label="autofps Stream" icon="camera"{Video url="http://192.168.0.2:54321/autofps.mjpeg" encoding="mjpeg"}
|
||||
Text label="HLS Video Stream" icon="camera"{Video url="http://192.168.0.2:54321/ipcamera.m3u8" encoding="hls"}
|
||||
Text label="HLS Webview Stream" icon="camera"{Webview url="http://192.168.0.2:54321/ipcamera.m3u8" height=15}
|
||||
Text label="Image using JPG method" icon="camera"{Image url="http://192.168.0.2:54321/ipcamera.jpg" refresh=2000}
|
||||
}
|
||||
|
||||
Text label="Generic Camera" icon="camera"{
|
||||
Switch item=GenericMotionControl
|
||||
Slider item=GenericMotionControl
|
||||
Default item=GenericMotionAlarm
|
||||
Switch item=GenericAudioThreshold
|
||||
Slider item=GenericAudioThreshold
|
||||
Default item=GenericAudioAlarm
|
||||
Text label="MJPEG Stream" icon="camera"{Video url="http://192.168.0.2:54323/ipcamera.mjpeg" encoding="mjpeg"}
|
||||
Text label="snapshots 1FPS Stream" icon="camera"{Video url="http://192.168.0.2:54323/snapshots.mjpeg" encoding="mjpeg"}
|
||||
Text label="autofps Stream" icon="camera"{Video url="http://192.168.0.2:54323/autofps.mjpeg" encoding="mjpeg"}
|
||||
Text label="HLS Video Stream" icon="camera"{Video url="http://192.168.0.2:54323/ipcamera.m3u8" encoding="hls"}
|
||||
Text label="HLS Stream" icon="camera"{Webview url="http://192.168.0.2:54323/ipcamera.m3u8" height=15}
|
||||
Text label="Image JPG method" icon="camera"{Image url="http://192.168.0.2:54323/ipcamera.jpg" refresh=1000}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
*.rules
|
||||
|
||||
```java
|
||||
rule "Camera detected crying"
|
||||
when
|
||||
Item BabyCamAudioAlarm changed from OFF to ON
|
||||
then
|
||||
if(BabyMonitor.state==ON){
|
||||
|
||||
if(MumAlerts.state==ON){
|
||||
sendNotification("mum@parentCo.com", "Mum, the baby is awake.")
|
||||
}
|
||||
|
||||
if(DadAlerts.state==ON){
|
||||
sendNotification("dad@parentCo.com", "Dad, the baby is awake.")
|
||||
}
|
||||
|
||||
if(TvAlerts.state==ON){
|
||||
myKodi_notification.sendCommand("Baby is crying.")
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
rule "Create time of last movement"
|
||||
when
|
||||
Item BabyCamLastMotionType received update
|
||||
then
|
||||
BabyCamLastMotionTime.postUpdate( new DateTimeType() )
|
||||
end
|
||||
|
||||
```
|
||||
66
bundles/org.openhab.binding.ipcamera/pom.xml
Normal file
66
bundles/org.openhab.binding.ipcamera/pom.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.ipcamera</artifactId>
|
||||
<name>openHAB Add-ons :: Bundles :: IpCamera Binding</name>
|
||||
|
||||
<properties>
|
||||
<netty.version>4.1.42.Final</netty.version>
|
||||
<dep.noembedding>netty-common,netty-transport,netty-buffer,netty-resolver,netty-codec,netty-codec-http,netty-handler</dep.noembedding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec-http</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-common</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-buffer</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-transport</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-resolver</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-handler</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.ipcamera-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
<feature name="openhab-binding-ipcamera" description="ipcamera Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature dependency="true">openhab.tp-jaxb</feature>
|
||||
<feature dependency="true">openhab.tp-netty</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ipcamera/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link AmcrestHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class AmcrestHandler extends ChannelDuplexHandler {
|
||||
private String requestUrl = "Empty";
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public AmcrestHandler(ThingHandler handler) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
public void setURL(String url) {
|
||||
requestUrl = url;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String content = msg.toString();
|
||||
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
if (content.contains("Error: No Events")) {
|
||||
if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion".equals(requestUrl)) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation".equals(requestUrl)) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
} else if (content.contains("channels[0]=0")) {
|
||||
if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion".equals(requestUrl)) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation".equals(requestUrl)) {
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
}
|
||||
|
||||
if (content.contains("table.MotionDetect[0].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
} else if (content.contains("table.MotionDetect[0].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
}
|
||||
// determine if the audio alarm is turned on or off.
|
||||
if (content.contains("table.AudioDetect[0].MutationDetect=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.AudioDetect[0].MutationDetect=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle AudioMutationThreshold alarm
|
||||
if (content.contains("table.AudioDetect[0].MutationThreold=")) {
|
||||
String value = ipCameraHandler.returnValueFromString(content, "table.AudioDetect[0].MutationThreold=");
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value));
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=CrossLineDetection[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=MotionDetect[0]");
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=false");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].Text="
|
||||
+ text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LED:
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Off");
|
||||
} else if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual&Lighting[0][0].MiddleLight[0].Light="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Auto");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int threshold = Math.round(Float.valueOf(command.toString()));
|
||||
|
||||
if (threshold == 0) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=" + threshold);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=true&AudioDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=true");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=true&MotionDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link CameraConfig} handles the configuration of cameras.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CameraConfig {
|
||||
private String ipAddress = "";
|
||||
private String ffmpegInputOptions = "";
|
||||
private int port;
|
||||
private int onvifPort;
|
||||
private int serverPort;
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private int onvifMediaProfile;
|
||||
private int pollTime;
|
||||
private String ffmpegInput = "";
|
||||
private String snapshotUrl = "";
|
||||
private String mjpegUrl = "";
|
||||
private String alarmInputUrl = "";
|
||||
private String customMotionAlarmUrl = "";
|
||||
private String customAudioAlarmUrl = "";
|
||||
private String updateImageWhen = "";
|
||||
private int nvrChannel;
|
||||
private String ipWhitelist = "";
|
||||
private String ffmpegLocation = "";
|
||||
private String ffmpegOutput = "";
|
||||
private String hlsOutOptions = "";
|
||||
private String gifOutOptions = "";
|
||||
private String mp4OutOptions = "";
|
||||
private String mjpegOptions = "";
|
||||
private String motionOptions = "";
|
||||
private boolean ptzContinuous;
|
||||
private int gifPreroll;
|
||||
|
||||
public int getOnvifMediaProfile() {
|
||||
return onvifMediaProfile;
|
||||
}
|
||||
|
||||
public String getFfmpegInputOptions() {
|
||||
return ffmpegInputOptions;
|
||||
}
|
||||
|
||||
public String getMjpegOptions() {
|
||||
return mjpegOptions;
|
||||
}
|
||||
|
||||
public String getMotionOptions() {
|
||||
return motionOptions;
|
||||
}
|
||||
|
||||
public String getMp4OutOptions() {
|
||||
return mp4OutOptions;
|
||||
}
|
||||
|
||||
public String getGifOutOptions() {
|
||||
return gifOutOptions;
|
||||
}
|
||||
|
||||
public String getHlsOutOptions() {
|
||||
return hlsOutOptions;
|
||||
}
|
||||
|
||||
public String getIpWhitelist() {
|
||||
return ipWhitelist;
|
||||
}
|
||||
|
||||
public String getFfmpegLocation() {
|
||||
return ffmpegLocation;
|
||||
}
|
||||
|
||||
public String getFfmpegOutput() {
|
||||
return ffmpegOutput;
|
||||
}
|
||||
|
||||
public boolean getPtzContinuous() {
|
||||
return ptzContinuous;
|
||||
}
|
||||
|
||||
public String getAlarmInputUrl() {
|
||||
return alarmInputUrl;
|
||||
}
|
||||
|
||||
public String getCustomAudioAlarmUrl() {
|
||||
return customAudioAlarmUrl;
|
||||
}
|
||||
|
||||
public String getCustomMotionAlarmUrl() {
|
||||
return customMotionAlarmUrl;
|
||||
}
|
||||
|
||||
public int getNvrChannel() {
|
||||
return nvrChannel;
|
||||
}
|
||||
|
||||
public String getMjpegUrl() {
|
||||
return mjpegUrl;
|
||||
}
|
||||
|
||||
public String getSnapshotUrl() {
|
||||
return snapshotUrl;
|
||||
}
|
||||
|
||||
public String getFfmpegInput() {
|
||||
return ffmpegInput;
|
||||
}
|
||||
|
||||
public String getUpdateImageWhen() {
|
||||
return updateImageWhen;
|
||||
}
|
||||
|
||||
public int getPollTime() {
|
||||
return pollTime;
|
||||
}
|
||||
|
||||
public int getOnvifPort() {
|
||||
return onvifPort;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUser(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public int getGifPreroll() {
|
||||
return gifPreroll;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
* The {@link ChannelTracking} Can be used to find the handle for a HTTP channel if you know the URL. The reply can
|
||||
* optionally be stored for later use.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class ChannelTracking {
|
||||
private String storedReply = "";
|
||||
private String requestUrl = "";
|
||||
private Channel channel;
|
||||
|
||||
public ChannelTracking(Channel channel, String requestUrl) {
|
||||
this.channel = channel;
|
||||
this.requestUrl = requestUrl;
|
||||
}
|
||||
|
||||
public String getRequestUrl() {
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public String getReply() {
|
||||
return storedReply;
|
||||
}
|
||||
|
||||
public void setReply(String replyToStore) {
|
||||
storedReply = replyToStore;
|
||||
}
|
||||
|
||||
public void setChannel(Channel ch) {
|
||||
channel = ch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link DahuaHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DahuaHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private int nvrChannel;
|
||||
|
||||
public DahuaHandler(IpCameraHandler handler, int nvrChannel) {
|
||||
ipCameraHandler = handler;
|
||||
this.nvrChannel = nvrChannel;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
// determine if the motion detection is turned on or off.
|
||||
if (content.contains("table.MotionDetect[0].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.MotionDetect[" + nvrChannel + "].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle motion alarm
|
||||
if (content.contains("Code=VideoMotion;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (content.contains("Code=VideoMotion;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
// Handle item taken alarm
|
||||
if (content.contains("Code=TakenAwayDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_ITEM_TAKEN);
|
||||
} else if (content.contains("Code=TakenAwayDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_ITEM_TAKEN);
|
||||
}
|
||||
// Handle item left alarm
|
||||
if (content.contains("Code=LeftDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_ITEM_LEFT);
|
||||
} else if (content.contains("Code=LeftDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_ITEM_LEFT);
|
||||
}
|
||||
// Handle CrossLineDetection alarm
|
||||
if (content.contains("Code=CrossLineDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
} else if (content.contains("Code=CrossLineDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
}
|
||||
// determine if the audio alarm is turned on or off.
|
||||
if (content.contains("table.AudioDetect[0].MutationDetect=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.AudioDetect[0].MutationDetect=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle AudioMutation alarm
|
||||
if (content.contains("Code=AudioMutation;action=Start;index=0")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
} else if (content.contains("Code=AudioMutation;action=Stop;index=0")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
// Handle AudioMutationThreshold alarm
|
||||
if (content.contains("table.AudioDetect[0].MutationThreold=")) {
|
||||
String value = ipCameraHandler.returnValueFromString(content, "table.AudioDetect[0].MutationThreold=");
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value));
|
||||
}
|
||||
// Handle FaceDetection alarm
|
||||
if (content.contains("Code=FaceDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FACE_DETECTED);
|
||||
} else if (content.contains("Code=FaceDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FACE_DETECTED);
|
||||
}
|
||||
// Handle ParkingDetection alarm
|
||||
if (content.contains("Code=ParkingDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_PARKING_ALARM);
|
||||
} else if (content.contains("Code=ParkingDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_PARKING_ALARM);
|
||||
}
|
||||
// Handle CrossRegionDetection alarm
|
||||
if (content.contains("Code=CrossRegionDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
} else if (content.contains("Code=CrossRegionDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
}
|
||||
// Handle External Input alarm
|
||||
if (content.contains("Code=AlarmLocal;action=Start;index=0")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("Code=AlarmLocal;action=Stop;index=0")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
// Handle External Input alarm2
|
||||
if (content.contains("Code=AlarmLocal;action=Start;index=1")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.ON);
|
||||
} else if (content.contains("Code=AlarmLocal;action=Stop;index=1")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.OFF);
|
||||
}
|
||||
// CrossLineDetection alarm on/off
|
||||
if (content.contains("table.VideoAnalyseRule[0][1].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.VideoAnalyseRule[0][1].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
// ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=VideoAnalyseRule");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=MotionDetect[0]");
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=false");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].Text="
|
||||
+ text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LED:
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Off");
|
||||
} else if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual&Lighting[0][0].MiddleLight[0].Light="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Auto");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int threshold = Math.round(Float.valueOf(command.toString()));
|
||||
|
||||
if (threshold == 0) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=" + threshold);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=true&AudioDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=true");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=true&MotionDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString()) / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link DoorBirdHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DoorBirdHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public DoorBirdHandler(ThingHandler handler) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (content.contains("doorbell:H")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.ON);
|
||||
}
|
||||
if (content.contains("doorbell:L")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("motionsensor:L")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
if (content.contains("motionsensor:H")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
return;
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/open-door.cgi");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/open-door.cgi?r=2");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_EXTERNAL_LIGHT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/light-on.cgi");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link Ffmpeg} class is responsible for handling multiple ffmpeg conversions which are used for many tasks
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Ffmpeg {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private @Nullable Process process = null;
|
||||
private String ffmpegCommand = "";
|
||||
private FFmpegFormat format;
|
||||
private List<String> commandArrayList = new ArrayList<String>();
|
||||
private IpCameraFfmpegThread ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
private int keepAlive = 8;
|
||||
private boolean running = false;
|
||||
|
||||
public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
|
||||
String input, String outArguments, String output, String username, String password) {
|
||||
this.format = format;
|
||||
ipCameraHandler = handle;
|
||||
String altInput = input;
|
||||
// Input can be snapshots not just rtsp or http
|
||||
if (!password.isEmpty() && !input.contains("@") && input.contains("rtsp")) {
|
||||
String credentials = username + ":" + password + "@";
|
||||
// will not work for https: but currently binding does not use https
|
||||
altInput = input.substring(0, 7) + credentials + input.substring(7);
|
||||
}
|
||||
if (inputArguments.isEmpty()) {
|
||||
ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
|
||||
} else {
|
||||
ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
|
||||
}
|
||||
Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
|
||||
// ffmpegLocation may have a space in its folder
|
||||
commandArrayList.add(0, ffmpegLocation);
|
||||
}
|
||||
|
||||
public void setKeepAlive(int seconds) {
|
||||
if (seconds == -1) {
|
||||
keepAlive = -1;
|
||||
} else {// We now poll every 8 seconds due to mjpeg stream requirement.
|
||||
keepAlive = 8; // 64 seconds approx.
|
||||
}
|
||||
}
|
||||
|
||||
public void checkKeepAlive() {
|
||||
if (keepAlive <= -1) {
|
||||
return;
|
||||
} else if (keepAlive == 0) {
|
||||
stopConverting();
|
||||
} else {
|
||||
keepAlive--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private class IpCameraFfmpegThread extends Thread {
|
||||
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
|
||||
public int countOfMotions;
|
||||
|
||||
IpCameraFfmpegThread() {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
private void gifCreated() {
|
||||
// Without a small delay, Pushover sends no file 10% of time.
|
||||
ipCameraHandler.setChannelState(CHANNEL_RECORDING_GIF, DecimalType.ZERO);
|
||||
ipCameraHandler.setChannelState(CHANNEL_GIF_HISTORY_LENGTH,
|
||||
new DecimalType(++ipCameraHandler.gifHistoryLength));
|
||||
}
|
||||
|
||||
private void mp4Created() {
|
||||
ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
|
||||
ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
|
||||
new DecimalType(++ipCameraHandler.mp4HistoryLength));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
process = Runtime.getRuntime().exec(commandArrayList.toArray(new String[commandArrayList.size()]));
|
||||
if (process != null) {
|
||||
InputStream errorStream = process.getErrorStream();
|
||||
InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
|
||||
BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
|
||||
String line = null;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (format.equals(FFmpegFormat.RTSP_ALARMS)) {
|
||||
logger.debug("{}", line);
|
||||
if (line.contains("lavfi.")) {
|
||||
if (countOfMotions == 4) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
|
||||
} else {
|
||||
countOfMotions++;
|
||||
}
|
||||
} else if (line.contains("speed=")) {
|
||||
if (countOfMotions > 0) {
|
||||
countOfMotions--;
|
||||
countOfMotions--;
|
||||
if (countOfMotions <= 0) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
} else if (line.contains("silence_start")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
} else if (line.contains("silence_end")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
} else {
|
||||
logger.debug("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("An error occured trying to process the messages from FFmpeg.");
|
||||
} finally {
|
||||
switch (format) {
|
||||
case GIF:
|
||||
threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
|
||||
break;
|
||||
case RECORD:
|
||||
threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startConverting() {
|
||||
if (!ipCameraFfmpegThread.isAlive()) {
|
||||
ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
|
||||
ipCameraFfmpegThread.start();
|
||||
running = true;
|
||||
if (format.equals(FFmpegFormat.HLS)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
if (keepAlive != -1) {
|
||||
keepAlive = 8;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getIsAlive() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void stopConverting() {
|
||||
if (ipCameraFfmpegThread.isAlive()) {
|
||||
logger.debug("Stopping ffmpeg {} now", format);
|
||||
running = false;
|
||||
if (process != null) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
if (format.equals(FFmpegFormat.HLS)) {
|
||||
if (keepAlive == -1) {
|
||||
logger.warn("HLS stopped when Stream should be running non stop, restarting HLS now.");
|
||||
startConverting();
|
||||
return;
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
keepAlive = 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link FoscamHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class FoscamHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String password, username;
|
||||
|
||||
public FoscamHandler(ThingHandler handler, String username, String password) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
////////////// Motion Alarm //////////////
|
||||
if (content.contains("<motionDetectAlarm>")) {
|
||||
if (content.contains("<motionDetectAlarm>0</motionDetectAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
} else if (content.contains("<motionDetectAlarm>1</motionDetectAlarm>")) { // Enabled but no alarm
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (content.contains("<motionDetectAlarm>2</motionDetectAlarm>")) {// Enabled, alarm on
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
|
||||
////////////// Sound Alarm //////////////
|
||||
if (content.contains("<soundAlarm>0</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<soundAlarm>1</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
if (content.contains("<soundAlarm>2</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
|
||||
////////////// Sound Threshold //////////////
|
||||
if (content.contains("<sensitivity>0</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.ZERO);
|
||||
}
|
||||
if (content.contains("<sensitivity>1</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf("50"));
|
||||
}
|
||||
if (content.contains("<sensitivity>2</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.HUNDRED);
|
||||
}
|
||||
|
||||
//////////////// Infrared LED /////////////////////
|
||||
if (content.contains("<infraLedState>0</infraLedState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<infraLedState>1</infraLedState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, OnOffType.ON);
|
||||
}
|
||||
|
||||
if (content.contains("</CGI_Result>")) {
|
||||
ctx.close();
|
||||
ipCameraHandler.logger.debug("End of FOSCAM handler reached, so closing the channel to the camera now");
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=getAudioAlarmConfig&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=getAudioAlarmConfig&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ENABLE_LED:
|
||||
// Disable the auto mode first
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=1&usr=" + username + "&pwd=" + password);
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=closeInfraLed&usr=" + username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=openInfraLed&usr=" + username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=0&usr=" + username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=1&usr=" + username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int value = Math.round(Float.valueOf(command.toString()));
|
||||
if (value == 0) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else if (value <= 33) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else if (value <= 66) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=2&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
if (ipCameraHandler.cameraConfig.getCustomAudioAlarmUrl().isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.cameraConfig.getCustomAudioAlarmUrl());
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
if (ipCameraHandler.cameraConfig.getCustomMotionAlarmUrl().isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig1&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.cameraConfig.getCustomMotionAlarmUrl());
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig1&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
lowPriorityRequests.add("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + username + "&pwd=" + password);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link GroupConfig} handles the configuration of camera groups.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GroupConfig {
|
||||
private int pollTime, serverPort;
|
||||
private boolean motionChangesOrder = true;
|
||||
private String ipWhitelist = "";
|
||||
private String ffmpegLocation = "";
|
||||
private String ffmpegOutput = "";
|
||||
private String firstCamera = "";
|
||||
private String secondCamera = "";
|
||||
private String thirdCamera = "";
|
||||
private String forthCamera = "";
|
||||
|
||||
public String getFirstCamera() {
|
||||
return firstCamera;
|
||||
}
|
||||
|
||||
public String getSecondCamera() {
|
||||
return secondCamera;
|
||||
}
|
||||
|
||||
public String getThirdCamera() {
|
||||
return thirdCamera;
|
||||
}
|
||||
|
||||
public String getForthCamera() {
|
||||
return forthCamera;
|
||||
}
|
||||
|
||||
public boolean getMotionChangesOrder() {
|
||||
return motionChangesOrder;
|
||||
}
|
||||
|
||||
public String getIpWhitelist() {
|
||||
return ipWhitelist;
|
||||
}
|
||||
|
||||
public String getFfmpegLocation() {
|
||||
return ffmpegLocation;
|
||||
}
|
||||
|
||||
public String getFfmpegOutput() {
|
||||
return ffmpegOutput;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public int getPollTime() {
|
||||
return pollTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
|
||||
/**
|
||||
* The {@link GroupTracker} is used so a 'group' thing can get a handle to each cameras handler, and the group and
|
||||
* cameras can talk to each other.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class GroupTracker {
|
||||
public ArrayList<IpCameraHandler> listOfOnlineCameraHandlers = new ArrayList<>(1);
|
||||
public ArrayList<IpCameraGroupHandler> listOfGroupHandlers = new ArrayList<>(0);
|
||||
public ArrayList<String> listOfOnlineCameraUID = new ArrayList<>(1);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link Helper} class has static functions that help the IpCamera binding not need as many external libs.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Helper {
|
||||
|
||||
/**
|
||||
* The {@link searchString} Used to grab values out of JSON or other quote encapsulated structures without needing
|
||||
* an external lib. String may be terminated by ," or }.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
public static String searchString(String rawString, String searchedString) {
|
||||
String result = "";
|
||||
int index = 0;
|
||||
index = rawString.indexOf(searchedString);
|
||||
if (index != -1) // -1 means "not found"
|
||||
{
|
||||
result = rawString.substring(index + searchedString.length(), rawString.length());
|
||||
index = result.indexOf(',');
|
||||
if (index == -1) {
|
||||
index = result.indexOf('"');
|
||||
if (index == -1) {
|
||||
index = result.indexOf('}');
|
||||
if (index == -1) {
|
||||
return result;
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
} else {
|
||||
result = result.substring(0, index);
|
||||
index = result.indexOf('"');
|
||||
if (index == -1) {
|
||||
return result;
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String fetchXML(String message, String sectionHeading, String key) {
|
||||
String result = "";
|
||||
int sectionHeaderBeginning = 0;
|
||||
if (!sectionHeading.isEmpty()) {// looking for a sectionHeading
|
||||
sectionHeaderBeginning = message.indexOf(sectionHeading);
|
||||
}
|
||||
if (sectionHeaderBeginning == -1) {
|
||||
return "";
|
||||
}
|
||||
int startIndex = message.indexOf(key, sectionHeaderBeginning + sectionHeading.length());
|
||||
if (startIndex == -1) {
|
||||
return "";
|
||||
}
|
||||
int endIndex = message.indexOf("<", startIndex + key.length());
|
||||
if (endIndex > startIndex) {
|
||||
result = message.substring(startIndex + key.length(), endIndex);
|
||||
}
|
||||
// remove any quotes and anything after the quote.
|
||||
sectionHeaderBeginning = result.indexOf("\"");
|
||||
if (sectionHeaderBeginning > 0) {
|
||||
result = result.substring(0, sectionHeaderBeginning);
|
||||
}
|
||||
// remove any ">" and anything after it.
|
||||
sectionHeaderBeginning = result.indexOf(">");
|
||||
if (sectionHeaderBeginning > 0) {
|
||||
result = result.substring(0, sectionHeaderBeginning);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link encodeSpecialChars} Is used to replace spaces with %20 in Strings meant for URL queries.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
public static String encodeSpecialChars(String text) {
|
||||
String processed = text;
|
||||
try {
|
||||
processed = URLEncoder.encode(text, "UTF-8").replace("+", "%20");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
public static String getLocalIpAddress() {
|
||||
String ipAddress = "";
|
||||
try {
|
||||
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
|
||||
.hasMoreElements();) {
|
||||
NetworkInterface networkInterface = enumNetworks.nextElement();
|
||||
for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
|
||||
.hasMoreElements();) {
|
||||
InetAddress inetAddress = enumIpAddr.nextElement();
|
||||
if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
|
||||
&& inetAddress.isSiteLocalAddress()) {
|
||||
ipAddress = inetAddress.getHostAddress().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
}
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link HikvisionHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class HikvisionHandler extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private int nvrChannel;
|
||||
private int lineCount, vmdCount, leftCount, takenCount, faceCount, pirCount, fieldCount;
|
||||
|
||||
public HikvisionHandler(ThingHandler handler, int nvrChannel) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
this.nvrChannel = nvrChannel;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
int debounce = 3;
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
|
||||
if (content.contains("--boundary")) {// Alarm checking goes in here//
|
||||
if (content.contains("<EventNotificationAlert version=\"")) {
|
||||
if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
|
||||
if (content.contains("<eventType>linedetection</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
lineCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>fielddetection</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
fieldCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>VMD</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
vmdCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>facedetection</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
|
||||
faceCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>unattendedBaggage</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
|
||||
leftCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>attendedBaggage</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
|
||||
takenCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>PIR</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
|
||||
pirCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
} else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all channels
|
||||
if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
}
|
||||
countDown();
|
||||
}
|
||||
} else {
|
||||
String replyElement = Helper.fetchXML(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<");
|
||||
switch (replyElement) {
|
||||
case "MotionDetection version=":
|
||||
ipCameraHandler.storeHttpReply(
|
||||
"/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "IOInputPort version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/System/IO/inputs/" + nvrChannel, content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<triggering>low</triggering>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
} else if (content.contains("<triggering>high</triggering>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
}
|
||||
break;
|
||||
case "LineDetection":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "TextOverlay version=":
|
||||
ipCameraHandler.storeHttpReply(
|
||||
"/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1", content);
|
||||
String text = Helper.fetchXML(content, "<enabled>true</enabled>", "<displayText>");
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(text));
|
||||
break;
|
||||
case "AudioDetection version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
|
||||
content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "IOPortStatus version=":
|
||||
if (content.contains("<ioState>active</ioState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("<ioState>inactive</ioState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "FieldDetection version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "ResponseStatus version=":
|
||||
////////////////// External Alarm Input ///////////////
|
||||
if (content.contains(
|
||||
"<requestURL>/ISAPI/System/IO/inputs/" + nvrChannel + "/status</requestURL>")) {
|
||||
// Stops checking the external alarm if camera does not have feature.
|
||||
if (content.contains("<statusString>Invalid Operation</statusString>")) {
|
||||
ipCameraHandler.lowPriorityRequests.remove(0);
|
||||
ipCameraHandler.logger.debug(
|
||||
"Stopping checks for alarm inputs as camera appears to be missing this feature.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (content.contains("<EventNotificationAlert")) {
|
||||
if (content.contains("hannelID>" + nvrChannel + "</")
|
||||
|| content.contains("<channelID>0</channelID>")) {// some camera use c or
|
||||
// <dynChannelID>
|
||||
if (content.contains(
|
||||
"<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
countDown();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unhandled reply-{}.", content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This does debouncing of the alarms
|
||||
void countDown() {
|
||||
|
||||
if (lineCount > 1) {
|
||||
lineCount--;
|
||||
} else if (lineCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
lineCount--;
|
||||
}
|
||||
if (vmdCount > 1) {
|
||||
vmdCount--;
|
||||
} else if (vmdCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
|
||||
vmdCount--;
|
||||
}
|
||||
if (leftCount > 1) {
|
||||
leftCount--;
|
||||
} else if (leftCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
|
||||
leftCount--;
|
||||
}
|
||||
if (takenCount > 1) {
|
||||
takenCount--;
|
||||
} else if (takenCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
|
||||
takenCount--;
|
||||
}
|
||||
if (faceCount > 1) {
|
||||
faceCount--;
|
||||
} else if (faceCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
|
||||
faceCount--;
|
||||
}
|
||||
if (pirCount > 1) {
|
||||
pirCount--;
|
||||
} else if (pirCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
|
||||
pirCount--;
|
||||
}
|
||||
if (fieldCount > 1) {
|
||||
fieldCount--;
|
||||
} else if (fieldCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
|
||||
fieldCount--;
|
||||
}
|
||||
if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
|
||||
&& lineCount == 0) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
|
||||
public void hikSendXml(String httpPutURL, String xml) {
|
||||
logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
|
||||
request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
|
||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
|
||||
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
ipCameraHandler.sendHttpPUT(httpPutURL, request);
|
||||
}
|
||||
|
||||
public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
|
||||
ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
|
||||
if (localTracker == null) {
|
||||
ipCameraHandler.sendHttpGET(httpGetPutURL);
|
||||
logger.debug(
|
||||
"Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
|
||||
return;
|
||||
}
|
||||
String body = localTracker.getReply();
|
||||
if (body.isEmpty()) {
|
||||
logger.debug(
|
||||
"Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
|
||||
ipCameraHandler.sendHttpGET(httpGetPutURL);
|
||||
} else {
|
||||
logger.trace("An OLD reply from the camera was:{}", body);
|
||||
if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
|
||||
body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
|
||||
}
|
||||
int elementIndexStart = body.indexOf("<" + removeElement + ">");
|
||||
int elementIndexEnd = body.indexOf("</" + removeElement + ">");
|
||||
body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
|
||||
+ body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
|
||||
logger.trace("Body for this PUT is going to be:{}", body);
|
||||
localTracker.setReply(body);
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
|
||||
httpGetPutURL);
|
||||
request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
|
||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
|
||||
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
|
||||
ipCameraHandler.logger.debug("FieldDetection command");
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
|
||||
return;
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
|
||||
return;
|
||||
case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
logger.debug("Changing text overlay to {}", command.toString());
|
||||
if (command.toString().isEmpty()) {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"enabled", "<enabled>false</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"displayText", "<displayText>" + command.toString() + "</displayText>");
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"enabled", "<enabled>true</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
|
||||
logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
|
||||
if (OnOffType.OFF.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
|
||||
"<triggering>low</triggering>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
|
||||
"<triggering>high</triggering>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_PIR_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
|
||||
"enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
|
||||
"enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
|
||||
"<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>high</outputState>\r\n</IOPortData>\r\n");
|
||||
} else {
|
||||
hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
|
||||
"<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_THRESHOLD_AUDIO_ALARM;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link HttpOnlyHandler} is responsible for handling commands for generic and onvif thing types.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class HttpOnlyHandler extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public HttpOnlyHandler(IpCameraHandler handler) {
|
||||
ipCameraHandler = handler;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.audioAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.audioAlarmEnabled = false;
|
||||
} else {
|
||||
ipCameraHandler.audioAlarmEnabled = true;
|
||||
try {
|
||||
ipCameraHandler.audioThreshold = Integer.valueOf(command.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Audio Threshold recieved an unexpected command, was it a number?");
|
||||
}
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list and sends 1 every 8 seconds.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
return new ArrayList<String>(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link InstarHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class InstarHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String requestUrl = "Empty";
|
||||
|
||||
public InstarHandler(ThingHandler thingHandler) {
|
||||
ipCameraHandler = (IpCameraHandler) thingHandler;
|
||||
}
|
||||
|
||||
public void setURL(String url) {
|
||||
requestUrl = url;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
String value1 = "";
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
switch (requestUrl) {
|
||||
case "/param.cgi?cmd=getinfrared":
|
||||
if (content.contains("var infraredstat=\"auto")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "/param.cgi?cmd=getoverlayattr&-region=1":// Text Overlays
|
||||
if (content.contains("var show_1=\"0\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.EMPTY);
|
||||
} else {
|
||||
value1 = Helper.searchString(content, "var name_1=\"");
|
||||
if (!value1.isEmpty()) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(value1));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "/cgi-bin/hi3510/param.cgi?cmd=getmdattr":// Motion Alarm
|
||||
// Motion Alarm
|
||||
if (content.contains("var m1_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "/cgi-bin/hi3510/param.cgi?cmd=getaudioalarmattr":// Audio Alarm
|
||||
if (content.contains("var aa_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
value1 = Helper.searchString(content, "var aa_value=\"");
|
||||
if (!value1.isEmpty()) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value1));
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "param.cgi?cmd=getpirattr":// PIR Alarm
|
||||
if (content.contains("var pir_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_PIR_ALARM, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_PIR_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Reset the Alarm, need to find better place to put this.
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_PIR_ALARM);
|
||||
break;
|
||||
case "/param.cgi?cmd=getioattr":// External Alarm Input
|
||||
if (content.contains("var io_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_MOTION_ALARM:
|
||||
if (ipCameraHandler.cameraConfig.getServerPort() > 0) {
|
||||
ipCameraHandler.logger.info("Setting up the Alarm Server settings in the camera now");
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
|
||||
+ ipCameraHandler.hostIp + "&-as_port="
|
||||
+ ipCameraHandler.cameraConfig.getServerPort()
|
||||
+ "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int value = Math.round(Float.valueOf(command.toString()));
|
||||
if (value == 0) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=0");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1");
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1&-aa_value="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/hi3510/param.cgi?cmd=setmdattr&-enable=1&-name=1&cmd=setmdattr&-enable=1&-name=2&cmd=setmdattr&-enable=1&-name=3&cmd=setmdattr&-enable=1&-name=4");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/hi3510/param.cgi?cmd=setmdattr&-enable=0&-name=1&cmd=setmdattr&-enable=0&-name=2&cmd=setmdattr&-enable=0&-name=3&cmd=setmdattr&-enable=0&-name=4");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setoverlayattr&-region=1&-show=0");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setoverlayattr&-region=1&-show=1&-name=" + text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setinfrared&-infraredstat=auto");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setinfrared&-infraredstat=close");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_PIR_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setpirattr&-pir_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setpirattr&-pir_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setioattr&-io_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setioattr&-io_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void alarmTriggered(String alarm) {
|
||||
ipCameraHandler.logger.debug("Alarm has been triggered:{}", alarm);
|
||||
switch (alarm) {
|
||||
case "/instar?&active=1":// The motion area boxes 1-4
|
||||
case "/instar?&active=2":
|
||||
case "/instar?&active=3":
|
||||
case "/instar?&active=4":
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
break;
|
||||
case "/instar?&active=5":// PIR
|
||||
ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
|
||||
break;
|
||||
case "/instar?&active=6":// Audio Alarm
|
||||
ipCameraHandler.audioDetected();
|
||||
break;
|
||||
case "/instar?&active=7":// Motion Area 1
|
||||
case "/instar?&active=8":// Motion Area 2
|
||||
case "/instar?&active=9":// Motion Area 3
|
||||
case "/instar?&active=10":// Motion Area 4
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(2);
|
||||
lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getaudioalarmattr");
|
||||
lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getmdattr");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getinfrared");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getoverlayattr&-region=1");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getpirattr");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getioattr"); // ext alarm input on/off
|
||||
// lowPriorityRequests.add("/param.cgi?cmd=getserverinfo");
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
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 {@link IpCameraActions} is responsible for Actions.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@ThingActionsScope(name = "ipcamera")
|
||||
@NonNullByDefault
|
||||
public class IpCameraActions implements ThingActions {
|
||||
public final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable IpCameraHandler handler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
this.handler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@RuleAction(label = "Record MP4", description = "Record MP4 to a set filename if given, or if filename is null to ipcamera.mp4")
|
||||
public void recordMP4(
|
||||
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
|
||||
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
|
||||
logger.debug("Recording {}.mp4 for {} seconds.", filename, secondsToRecord);
|
||||
if (filename == null && handler != null) {
|
||||
handler.recordMp4("ipcamera", secondsToRecord);
|
||||
} else if (handler != null && filename != null) {
|
||||
handler.recordMp4(filename, secondsToRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recordMP4(@Nullable ThingActions actions, @Nullable String filename, int secondsToRecord) {
|
||||
if (actions instanceof IpCameraActions) {
|
||||
((IpCameraActions) actions).recordMP4(filename, secondsToRecord);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a IpCamera class.");
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "Record GIF", description = "Record GIF to a set filename if given, or if filename is null to ipcamera.gif")
|
||||
public void recordGIF(
|
||||
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
|
||||
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
|
||||
logger.debug("Recording {}.gif for {} seconds.", filename, secondsToRecord);
|
||||
if (filename == null && handler != null) {
|
||||
handler.recordGif("ipcamera", secondsToRecord);
|
||||
} else if (handler != null && filename != null) {
|
||||
handler.recordGif(filename, secondsToRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recordGIF(@Nullable ThingActions actions, @Nullable String filename, int secondsToRecord) {
|
||||
if (actions instanceof IpCameraActions) {
|
||||
((IpCameraActions) actions).recordGIF(filename, secondsToRecord);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a IpCamera class.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraBindingConstants} class defines common constants, which
|
||||
* are used across the whole binding.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IpCameraBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "ipcamera";
|
||||
public final static String AUTH_HANDLER = "authorizationHandler";
|
||||
public final static String AMCREST_HANDLER = "amcrestHandler";
|
||||
public final static String COMMON_HANDLER = "commonHandler";
|
||||
public final static String INSTAR_HANDLER = "instarHandler";
|
||||
|
||||
public static enum FFmpegFormat {
|
||||
HLS,
|
||||
GIF,
|
||||
RECORD,
|
||||
RTSP_ALARMS,
|
||||
MJPEG,
|
||||
SNAPSHOT
|
||||
}
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
|
||||
public static final String GENERIC_THING = "generic";
|
||||
public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, GENERIC_THING);
|
||||
public static final String ONVIF_THING = "onvif";
|
||||
public static final ThingTypeUID THING_TYPE_ONVIF = new ThingTypeUID(BINDING_ID, ONVIF_THING);
|
||||
public static final String AMCREST_THING = "amcrest";
|
||||
public static final ThingTypeUID THING_TYPE_AMCREST = new ThingTypeUID(BINDING_ID, AMCREST_THING);
|
||||
public static final String FOSCAM_THING = "foscam";
|
||||
public static final ThingTypeUID THING_TYPE_FOSCAM = new ThingTypeUID(BINDING_ID, FOSCAM_THING);
|
||||
public static final String HIKVISION_THING = "hikvision";
|
||||
public static final ThingTypeUID THING_TYPE_HIKVISION = new ThingTypeUID(BINDING_ID, HIKVISION_THING);
|
||||
public static final String INSTAR_THING = "instar";
|
||||
public static final ThingTypeUID THING_TYPE_INSTAR = new ThingTypeUID(BINDING_ID, INSTAR_THING);
|
||||
public static final String DAHUA_THING = "dahua";
|
||||
public static final ThingTypeUID THING_TYPE_DAHUA = new ThingTypeUID(BINDING_ID, DAHUA_THING);
|
||||
public static final String DOORBIRD_THING = "doorbird";
|
||||
public static final ThingTypeUID THING_TYPE_DOORBIRD = new ThingTypeUID(BINDING_ID, DOORBIRD_THING);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = new HashSet<ThingTypeUID>(
|
||||
Arrays.asList(THING_TYPE_ONVIF, THING_TYPE_GENERIC, THING_TYPE_AMCREST, THING_TYPE_DAHUA, THING_TYPE_INSTAR,
|
||||
THING_TYPE_FOSCAM, THING_TYPE_DOORBIRD, THING_TYPE_HIKVISION));
|
||||
|
||||
public static final Set<ThingTypeUID> GROUP_SUPPORTED_THING_TYPES = new HashSet<ThingTypeUID>(
|
||||
Arrays.asList(THING_TYPE_GROUP));
|
||||
|
||||
// List of all Thing Config items
|
||||
public static final String CONFIG_IPADDRESS = "ipAddress";
|
||||
public static final String CONFIG_PORT = "port";
|
||||
public static final String CONFIG_ONVIF_PORT = "onvifPort";
|
||||
public static final String CONFIG_SERVER_PORT = "serverPort";
|
||||
public static final String CONFIG_USERNAME = "username";
|
||||
public static final String CONFIG_PASSWORD = "password";
|
||||
public static final String CONFIG_ONVIF_PROFILE_NUMBER = "onvifMediaProfile";
|
||||
public static final String CONFIG_POLL_TIME = "pollTime";
|
||||
public static final String CONFIG_FFMPEG_INPUT = "ffmpegInput";
|
||||
public static final String CONFIG_SNAPSHOT_URL_OVERRIDE = "snapshotUrl";
|
||||
public static final String CONFIG_MJPEG_URL = "mjpegUrl";
|
||||
public static final String CONFIG_FFMPEG_MOTION_INPUT = "alarmInputUrl";
|
||||
public static final String CONFIG_MOTION_URL_OVERRIDE = "customMotionAlarmUrl";
|
||||
public static final String CONFIG_AUDIO_URL_OVERRIDE = "customAudioAlarmUrl";
|
||||
public static final String CONFIG_IMAGE_UPDATE_WHEN = "updateImageWhen";
|
||||
public static final String CONFIG_NVR_CHANNEL = "nvrChannel";
|
||||
public static final String CONFIG_IP_WHITELIST = "ipWhitelist";
|
||||
public static final String CONFIG_FFMPEG_LOCATION = "ffmpegLocation";
|
||||
public static final String CONFIG_FFMPEG_OUTPUT = "ffmpegOutput";
|
||||
public static final String CONFIG_FFMPEG_HLS_OUT_ARGUMENTS = "hlsOutOptions";
|
||||
public static final String CONFIG_FFMPEG_GIF_OUT_ARGUMENTS = "gifOutOptions";
|
||||
public static final String CONFIG_FFMPEG_MP4_OUT_ARGUMENTS = "mp4OutOptions";
|
||||
public static final String CONFIG_FFMPEG_MJPEG_ARGUMENTS = "mjpegOptions";
|
||||
public static final String CONFIG_FFMPEG_MOTION_ARGUMENTS = "motionOptions";
|
||||
public static final String CONFIG_PTZ_CONTINUOUS = "ptzContinuous";
|
||||
public static final String CONFIG_GIF_PREROLL = "gifPreroll";
|
||||
// group thing configs
|
||||
public static final String CONFIG_FIRST_CAM = "firstCamera";
|
||||
public static final String CONFIG_SECOND_CAM = "secondCamera";
|
||||
public static final String CONFIG_THIRD_CAM = "thirdCamera";
|
||||
public static final String CONFIG_FORTH_CAM = "forthCamera";
|
||||
public static final String CONFIG_MOTION_CHANGES_ORDER = "motionChangesOrder";
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_POLL_IMAGE = "pollImage";
|
||||
public static final String CHANNEL_RECORDING_GIF = "recordingGif";
|
||||
public static final String CHANNEL_GIF_HISTORY = "gifHistory";
|
||||
public static final String CHANNEL_GIF_HISTORY_LENGTH = "gifHistoryLength";
|
||||
public static final String CHANNEL_RECORDING_MP4 = "recordingMp4";
|
||||
public static final String CHANNEL_MP4_PREROLL = "mp4Preroll";
|
||||
public static final String CHANNEL_MP4_HISTORY = "mp4History";
|
||||
public static final String CHANNEL_MP4_HISTORY_LENGTH = "mp4HistoryLength";
|
||||
public static final String CHANNEL_IMAGE = "image";
|
||||
public static final String CHANNEL_RTSP_URL = "rtspUrl";
|
||||
public static final String CHANNEL_IMAGE_URL = "imageUrl";
|
||||
public static final String CHANNEL_MJPEG_URL = "mjpegUrl";
|
||||
public static final String CHANNEL_HLS_URL = "hlsUrl";
|
||||
public static final String CHANNEL_PAN = "pan";
|
||||
public static final String CHANNEL_TILT = "tilt";
|
||||
public static final String CHANNEL_ZOOM = "zoom";
|
||||
public static final String CHANNEL_EXTERNAL_MOTION = "externalMotion";
|
||||
public static final String CHANNEL_MOTION_ALARM = "motionAlarm";
|
||||
public static final String CHANNEL_LINE_CROSSING_ALARM = "lineCrossingAlarm";
|
||||
public static final String CHANNEL_FACE_DETECTED = "faceDetected";
|
||||
public static final String CHANNEL_ITEM_LEFT = "itemLeft";
|
||||
public static final String CHANNEL_ITEM_TAKEN = "itemTaken";
|
||||
public static final String CHANNEL_AUDIO_ALARM = "audioAlarm";
|
||||
public static final String CHANNEL_ENABLE_MOTION_ALARM = "enableMotionAlarm";
|
||||
public static final String CHANNEL_FFMPEG_MOTION_CONTROL = "ffmpegMotionControl";
|
||||
public static final String CHANNEL_FFMPEG_MOTION_ALARM = "ffmpegMotionAlarm";
|
||||
public static final String CHANNEL_ENABLE_LINE_CROSSING_ALARM = "enableLineCrossingAlarm";
|
||||
public static final String CHANNEL_ENABLE_AUDIO_ALARM = "enableAudioAlarm";
|
||||
public static final String CHANNEL_THRESHOLD_AUDIO_ALARM = "thresholdAudioAlarm";
|
||||
public static final String CHANNEL_ACTIVATE_ALARM_OUTPUT = "activateAlarmOutput";
|
||||
public static final String CHANNEL_ACTIVATE_ALARM_OUTPUT2 = "activateAlarmOutput2";
|
||||
public static final String CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT = "enableExternalAlarmInput";
|
||||
public static final String CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT = "triggerExternalAlarmInput";
|
||||
public static final String CHANNEL_EXTERNAL_ALARM_INPUT = "externalAlarmInput";
|
||||
public static final String CHANNEL_EXTERNAL_ALARM_INPUT2 = "externalAlarmInput2";
|
||||
public static final String CHANNEL_AUTO_LED = "autoLED";
|
||||
public static final String CHANNEL_ENABLE_LED = "enableLED";
|
||||
public static final String CHANNEL_ENABLE_PIR_ALARM = "enablePirAlarm";
|
||||
public static final String CHANNEL_PIR_ALARM = "pirAlarm";
|
||||
public static final String CHANNEL_CELL_MOTION_ALARM = "cellMotionAlarm";
|
||||
public static final String CHANNEL_ENABLE_FIELD_DETECTION_ALARM = "enableFieldDetectionAlarm";
|
||||
public static final String CHANNEL_FIELD_DETECTION_ALARM = "fieldDetectionAlarm";
|
||||
public static final String CHANNEL_PARKING_ALARM = "parkingAlarm";
|
||||
public static final String CHANNEL_TAMPER_ALARM = "tamperAlarm";
|
||||
public static final String CHANNEL_TOO_DARK_ALARM = "tooDarkAlarm";
|
||||
public static final String CHANNEL_STORAGE_ALARM = "storageAlarm";
|
||||
public static final String CHANNEL_SCENE_CHANGE_ALARM = "sceneChangeAlarm";
|
||||
public static final String CHANNEL_TOO_BRIGHT_ALARM = "tooBrightAlarm";
|
||||
public static final String CHANNEL_TOO_BLURRY_ALARM = "tooBlurryAlarm";
|
||||
public static final String CHANNEL_TEXT_OVERLAY = "textOverlay";
|
||||
public static final String CHANNEL_EXTERNAL_LIGHT = "externalLight";
|
||||
public static final String CHANNEL_DOORBELL = "doorBell";
|
||||
public static final String CHANNEL_LAST_MOTION_TYPE = "lastMotionType";
|
||||
public static final String CHANNEL_GOTO_PRESET = "gotoPreset";
|
||||
public static final String CHANNEL_START_STREAM = "startStream";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ipcamera.internal.onvif.OnvifDiscovery;
|
||||
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.config.discovery.DiscoveryService;
|
||||
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 IpCameraDiscoveryService} is responsible for auto finding cameras that have Onvif
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "binding.ipcamera")
|
||||
public class IpCameraDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(IpCameraDiscoveryService.class);
|
||||
|
||||
public IpCameraDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES, 30, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
public void newCameraFound(String brand, String hostname, int onvifPort) {
|
||||
ThingTypeUID thingtypeuid = new ThingTypeUID("ipcamera", brand);
|
||||
ThingUID thingUID = new ThingUID(thingtypeuid, hostname.replace(".", ""));
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(CONFIG_IPADDRESS, hostname).withProperty(CONFIG_ONVIF_PORT, onvifPort)
|
||||
.withLabel(brand + " camera @" + hostname).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
OnvifDiscovery onvifDiscovery = new OnvifDiscovery(this);
|
||||
try {
|
||||
onvifDiscovery.discoverCameras(3702);// WS discovery
|
||||
onvifDiscovery.discoverCameras(1900);// SSDP
|
||||
} catch (UnknownHostException | InterruptedException e) {
|
||||
logger.warn(
|
||||
"IpCamera Discovery has an issue discovering the network settings to find cameras with. Try setting up the camera manually.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, immediate = true, configurationPid = "binding.ipcamera")
|
||||
@NonNullByDefault
|
||||
public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final @Nullable String openhabIpAddress;
|
||||
private final GroupTracker groupTracker = new GroupTracker();
|
||||
|
||||
@Activate
|
||||
public IpCameraHandlerFactory(final @Reference NetworkAddressService networkAddressService) {
|
||||
openhabIpAddress = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
if (SUPPORTED_THING_TYPES.contains(thingTypeUID) || GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraHandler(thing, openhabIpAddress, groupTracker);
|
||||
} else if (GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraGroupHandler(thing, openhabIpAddress, groupTracker);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
|
||||
/**
|
||||
* The {@link MyNettyAuthHandler} is responsible for handling the basic and digest auths
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class MyNettyAuthHandler extends ChannelDuplexHandler {
|
||||
public final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String username, password;
|
||||
private String httpMethod = "", httpUrl = "";
|
||||
private byte ncCounter = 0;
|
||||
private String nonce = "", opaque = "", qop = "";
|
||||
private String realm = "";
|
||||
|
||||
public MyNettyAuthHandler(String user, String pass, IpCameraHandler handle) {
|
||||
ipCameraHandler = handle;
|
||||
username = user;
|
||||
password = pass;
|
||||
}
|
||||
|
||||
public void setURL(String method, String url) {
|
||||
httpUrl = url;
|
||||
httpMethod = method;
|
||||
}
|
||||
|
||||
private String calcMD5Hash(String toHash) {
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||
byte[] array = messageDigest.digest(toHash.getBytes());
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
for (int i = 0; i < array.length; ++i) {
|
||||
stringBuffer.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.warn("NoSuchAlgorithmException error when calculating MD5 hash");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Method can be used a few ways. processAuth(null, string,string, false) to return the digest on demand, and
|
||||
// processAuth(challString, string,string, true) to auto send new packet
|
||||
// First run it should not have authenticate as null
|
||||
// nonce is reused if authenticate is null so the NC needs to increment to allow this//
|
||||
public void processAuth(String authenticate, String httpMethod, String requestURI, boolean reSend) {
|
||||
if (authenticate.contains("Basic realm=\"")) {
|
||||
if (ipCameraHandler.useDigestAuth == true) {
|
||||
// Possible downgrade authenticate attack avoided.
|
||||
return;
|
||||
}
|
||||
logger.debug("Setting up the camera to use Basic Auth and resending last request with correct auth.");
|
||||
if (ipCameraHandler.setBasicAuth(true)) {
|
||||
ipCameraHandler.sendHttpRequest(httpMethod, requestURI, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/////// Fresh Digest Authenticate method follows as Basic is already handled and returned ////////
|
||||
realm = Helper.searchString(authenticate, "realm=\"");
|
||||
if (realm.isEmpty()) {
|
||||
logger.warn("Could not find a valid WWW-Authenticate response in :{}", authenticate);
|
||||
return;
|
||||
}
|
||||
nonce = Helper.searchString(authenticate, "nonce=\"");
|
||||
opaque = Helper.searchString(authenticate, "opaque=\"");
|
||||
qop = Helper.searchString(authenticate, "qop=\"");
|
||||
|
||||
if (!qop.isEmpty() && !realm.isEmpty()) {
|
||||
ipCameraHandler.useDigestAuth = true;
|
||||
} else {
|
||||
logger.warn(
|
||||
"!!!! Something is wrong with the reply back from the camera. WWW-Authenticate header: qop:{}, realm:{}",
|
||||
qop, realm);
|
||||
}
|
||||
|
||||
String stale = Helper.searchString(authenticate, "stale=\"");
|
||||
if (stale.isEmpty()) {
|
||||
} else if (stale.equalsIgnoreCase("true")) {
|
||||
logger.debug("Camera reported stale=true which normally means the NONCE has expired.");
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
ipCameraHandler.cameraConfigError("Camera gave a 401 reply: You need to provide a password.");
|
||||
return;
|
||||
}
|
||||
// create the MD5 hashes
|
||||
String ha1 = username + ":" + realm + ":" + password;
|
||||
ha1 = calcMD5Hash(ha1);
|
||||
Random random = new Random();
|
||||
String cnonce = Integer.toHexString(random.nextInt());
|
||||
ncCounter = (ncCounter > 125) ? 1 : ++ncCounter;
|
||||
String nc = String.format("%08X", ncCounter); // 8 digit hex number
|
||||
String ha2 = httpMethod + ":" + requestURI;
|
||||
ha2 = calcMD5Hash(ha2);
|
||||
|
||||
String response = ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2;
|
||||
response = calcMD5Hash(response);
|
||||
|
||||
String digestString = "username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\""
|
||||
+ requestURI + "\", cnonce=\"" + cnonce + "\", nc=" + nc + ", qop=\"" + qop + "\", response=\""
|
||||
+ response + "\", opaque=\"" + opaque + "\"";
|
||||
|
||||
if (reSend) {
|
||||
ipCameraHandler.sendHttpRequest(httpMethod, requestURI, digestString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
boolean closeConnection = true;
|
||||
String authenticate = "";
|
||||
if (msg instanceof HttpResponse) {
|
||||
HttpResponse response = (HttpResponse) msg;
|
||||
if (response.status().code() == 401) {
|
||||
if (!response.headers().isEmpty()) {
|
||||
for (CharSequence name : response.headers().names()) {
|
||||
for (CharSequence value : response.headers().getAll(name)) {
|
||||
if (name.toString().equalsIgnoreCase("WWW-Authenticate")) {
|
||||
authenticate = value.toString();
|
||||
}
|
||||
if (name.toString().equalsIgnoreCase("Connection")
|
||||
&& value.toString().contains("keep-alive")) {
|
||||
// closeConnection = false;
|
||||
// trial this for a while to see if it solves too many bytes with digest turned on.
|
||||
closeConnection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!authenticate.isEmpty()) {
|
||||
processAuth(authenticate, httpMethod, httpUrl, true);
|
||||
} else {
|
||||
ipCameraHandler.cameraConfigError(
|
||||
"Camera gave no WWW-Authenticate: Your login details must be wrong.");
|
||||
}
|
||||
if (closeConnection) {
|
||||
ctx.close();// needs to be here
|
||||
}
|
||||
}
|
||||
} else if (response.status().code() != 200) {
|
||||
logger.debug("Camera at IP:{} gave a reply with a response code of :{}",
|
||||
ipCameraHandler.cameraConfig.getIp(), response.status().code());
|
||||
}
|
||||
}
|
||||
// Pass the Message back to the pipeline for the next handler to process//
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_START_STREAM;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerGroupHandler} class is responsible for handling streams and sending any requested files to
|
||||
* Openhabs
|
||||
* features for a group of cameras instead of individual cameras.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerGroupHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraGroupHandler ipCameraGroupHandler;
|
||||
private String whiteList = "";
|
||||
|
||||
public StreamServerGroupHandler(IpCameraGroupHandler ipCameraGroupHandler) {
|
||||
this.ipCameraGroupHandler = ipCameraGroupHandler;
|
||||
whiteList = ipCameraGroupHandler.groupConfig.getIpWhitelist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
private String resolveIndexToPath(String uri) {
|
||||
if (!uri.substring(1, 2).equals("i")) {
|
||||
return ipCameraGroupHandler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
|
||||
}
|
||||
return "notFound";
|
||||
// example is /1ipcameraxx.ts
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP) && !whiteList.equals("DISABLE")) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.", requestIP);
|
||||
return;
|
||||
} else if (HttpMethod.GET.equals(httpRequest.method())) {
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (ipCameraGroupHandler.hlsTurnedOn) {
|
||||
String debugMe = ipCameraGroupHandler.getPlayList();
|
||||
logger.debug("playlist is:{}", debugMe);
|
||||
sendString(ctx, debugMe, "application/x-mpegurl");
|
||||
return;
|
||||
} else {
|
||||
logger.warn(
|
||||
"HLS requires the groups startStream channel to be turned on first. Just starting it now.");
|
||||
String channelPrefix = "ipcamera:" + ipCameraGroupHandler.getThing().getThingTypeUID()
|
||||
+ ":" + ipCameraGroupHandler.getThing().getUID().getId() + ":";
|
||||
ipCameraGroupHandler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM),
|
||||
OnOffType.ON);
|
||||
}
|
||||
break;
|
||||
case "/ipcamera.jpg":
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, resolveIndexToPath(httpRequest.uri()) + httpRequest.uri().substring(2),
|
||||
"video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
sendFile(ctx, httpRequest.uri(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, httpRequest.uri(), "video/mp4");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
if (ipCameraGroupHandler.cameraIndex >= ipCameraGroupHandler.cameraOrder.size()) {
|
||||
logger.debug("WARN: Openhab may still be starting, or all cameras in the group are OFFLINE.");
|
||||
return;
|
||||
}
|
||||
IpCameraHandler handler = ipCameraGroupHandler.cameraOrder.get(ipCameraGroupHandler.cameraIndex);
|
||||
handler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(handler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
handler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
logger.trace("file is :{}", fileUri);
|
||||
File file = new File(fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
private void sendString(ChannelHandlerContext ctx, String contents, String contentType) {
|
||||
ByteBuf contentsBbuf = Unpooled.copiedBuffer(contents, 0, contents.length(), StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentsBbuf.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(contentsBbuf);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (cause == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.debug("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (evt == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerHandler} class is responsible for handling streams and sending any requested files to openHABs
|
||||
* features.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private boolean handlingMjpeg = false; // used to remove ctx from group when handler is removed.
|
||||
private boolean handlingSnapshotStream = false; // used to remove ctx from group when handler is removed.
|
||||
private byte[] incomingJpeg = new byte[0];
|
||||
private String whiteList = "";
|
||||
private int recievedBytes = 0;
|
||||
private boolean updateSnapshot = false;
|
||||
private boolean onvifEvent = false;
|
||||
|
||||
public StreamServerHandler(IpCameraHandler ipCameraHandler) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
whiteList = ipCameraHandler.getWhiteList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
if (!whiteList.equals("DISABLE")) {
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP)) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.",
|
||||
requestIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ("GET".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
logger.debug("Stream Server recieved request \tGET:{}", httpRequest.uri());
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
if (!ipCameraHandler.ffmpegHLS.getIsAlive()) {
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
ipCameraHandler.ffmpegHLS.startConverting();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
|
||||
}
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
ipCameraHandler.ffmpegHLS.setKeepAlive(8);
|
||||
}
|
||||
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
|
||||
ctx.close();
|
||||
return;
|
||||
case "/ipcamera.mpd":
|
||||
sendFile(ctx, httpRequest.uri(), "application/dash+xml");
|
||||
return;
|
||||
case "/ipcamera.gif":
|
||||
sendFile(ctx, httpRequest.uri(), "image/gif");
|
||||
return;
|
||||
case "/ipcamera.jpg":
|
||||
if (!ipCameraHandler.snapshotPolling && ipCameraHandler.snapshotUri != "") {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.snapshotUri);
|
||||
}
|
||||
if (ipCameraHandler.currentSnapshot.length == 1) {
|
||||
logger.warn("ipcamera.jpg was requested but there is no jpg in ram to send.");
|
||||
return;
|
||||
}
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
case "/snapshots.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.startSnapshotPolling();
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, false);
|
||||
return;
|
||||
case "/ipcamera.mjpeg":
|
||||
ipCameraHandler.setupMjpegStreaming(true, ctx);
|
||||
handlingMjpeg = true;
|
||||
return;
|
||||
case "/autofps.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, true);
|
||||
return;
|
||||
case "/instar":
|
||||
InstarHandler instar = new InstarHandler(ipCameraHandler);
|
||||
instar.alarmTriggered(httpRequest.uri().toString());
|
||||
ctx.close();
|
||||
return;
|
||||
case "/ipcamera0.ts":
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".gif")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/gif");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
// Allow access to the preroll and postroll jpg files
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/mp4");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if ("POST".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
switch (httpRequest.uri()) {
|
||||
case "/ipcamera.jpg":
|
||||
break;
|
||||
case "/snapshot.jpg":
|
||||
updateSnapshot = true;
|
||||
break;
|
||||
case "/OnvifEvent":
|
||||
onvifEvent = true;
|
||||
break;
|
||||
default:
|
||||
logger.debug("Stream Server recieved unknown request \tPOST:{}", httpRequest.uri());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
int index = 0;
|
||||
if (recievedBytes == 0) {
|
||||
incomingJpeg = new byte[content.content().capacity()];
|
||||
} else {
|
||||
byte[] temp = incomingJpeg;
|
||||
incomingJpeg = new byte[recievedBytes + content.content().capacity()];
|
||||
|
||||
for (; index < temp.length; index++) {
|
||||
incomingJpeg[index] = temp[index];
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < content.content().capacity(); i++) {
|
||||
incomingJpeg[index++] = content.content().getByte(i);
|
||||
}
|
||||
recievedBytes = incomingJpeg.length;
|
||||
if (content instanceof LastHttpContent) {
|
||||
if (updateSnapshot) {
|
||||
ipCameraHandler.processSnapshot(incomingJpeg);
|
||||
} else if (onvifEvent) {
|
||||
ipCameraHandler.onvifCamera.eventRecieved(new String(incomingJpeg, StandardCharsets.UTF_8));
|
||||
} else { // handles the snapshots that make up mjpeg from rtsp to ffmpeg conversions.
|
||||
if (recievedBytes > 1000) {
|
||||
ipCameraHandler.sendMjpegFrame(incomingJpeg, ipCameraHandler.mjpegChannelGroup);
|
||||
}
|
||||
}
|
||||
recievedBytes = 0;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
ipCameraHandler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(ipCameraHandler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
ipCameraHandler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
File file = new File(ipCameraHandler.cameraConfig.getFfmpegOutput() + fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (ctx == null || cause == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.trace("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
if (handlingMjpeg) {
|
||||
ipCameraHandler.setupMjpegStreaming(false, ctx);
|
||||
} else if (handlingSnapshotStream) {
|
||||
handlingSnapshotStream = false;
|
||||
ipCameraHandler.setupSnapshotStreaming(false, ctx, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.handler;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.GroupConfig;
|
||||
import org.openhab.binding.ipcamera.internal.GroupTracker;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.StreamServerGroupHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
|
||||
* group picture.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
public GroupConfig groupConfig;
|
||||
private BigDecimal pollTimeInSeconds = new BigDecimal(2);
|
||||
public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
|
||||
private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
|
||||
private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
|
||||
private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
|
||||
private @Nullable ServerBootstrap serverBootstrap;
|
||||
private @Nullable ChannelFuture serverFuture = null;
|
||||
public String hostIp;
|
||||
private boolean motionChangesOrder = true;
|
||||
public int serverPort = 0;
|
||||
public String playList = "";
|
||||
private String playingNow = "";
|
||||
public int cameraIndex = 0;
|
||||
public boolean hlsTurnedOn = false;
|
||||
private int entries = 0;
|
||||
private int mediaSequence = 1;
|
||||
private int discontinuitySequence = 0;
|
||||
private GroupTracker groupTracker;
|
||||
|
||||
public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker) {
|
||||
super(thing);
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
if (openhabIpAddress != null) {
|
||||
hostIp = openhabIpAddress;
|
||||
} else {
|
||||
hostIp = Helper.getLocalIpAddress();
|
||||
}
|
||||
this.groupTracker = groupTracker;
|
||||
}
|
||||
|
||||
public String getPlayList() {
|
||||
return playList;
|
||||
}
|
||||
|
||||
public String getOutputFolder(int index) {
|
||||
IpCameraHandler handle = cameraOrder.get(index);
|
||||
return handle.cameraConfig.getFfmpegOutput();
|
||||
}
|
||||
|
||||
private String readCamerasPlaylist(int cameraIndex) {
|
||||
String camerasm3u8 = "";
|
||||
IpCameraHandler handle = cameraOrder.get(cameraIndex);
|
||||
try {
|
||||
String file = handle.cameraConfig.getFfmpegOutput() + "ipcamera.m3u8";
|
||||
camerasm3u8 = new String(Files.readAllBytes(Paths.get(file)));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error occured fetching a groupDisplay cameras m3u8 file :{}", e.getMessage());
|
||||
}
|
||||
return camerasm3u8;
|
||||
}
|
||||
|
||||
String keepLast(String string, int numberToRetain) {
|
||||
int start = string.length();
|
||||
for (int loop = numberToRetain; loop > 0; loop--) {
|
||||
start = string.lastIndexOf("#EXTINF:", start - 1);
|
||||
if (start == -1) {
|
||||
logger.warn(
|
||||
"Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
entries = entries + numberToRetain;
|
||||
return string.substring(start);
|
||||
}
|
||||
|
||||
String removeFromStart(String string, int numberToRemove) {
|
||||
int startingFrom = string.indexOf("#EXTINF:");
|
||||
for (int loop = numberToRemove; loop > 0; loop--) {
|
||||
startingFrom = string.indexOf("#EXTINF:", startingFrom + 27);
|
||||
if (startingFrom == -1) {
|
||||
logger.warn(
|
||||
"Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
|
||||
return string;
|
||||
}
|
||||
}
|
||||
mediaSequence = mediaSequence + numberToRemove;
|
||||
entries = entries - numberToRemove;
|
||||
return string.substring(startingFrom);
|
||||
}
|
||||
|
||||
int howManySegments(String m3u8File) {
|
||||
int start = m3u8File.length();
|
||||
int numberOfFiles = 0;
|
||||
for (BigDecimal totalTime = new BigDecimal(0); totalTime.intValue() < pollTimeInSeconds
|
||||
.intValue(); numberOfFiles++) {
|
||||
start = m3u8File.lastIndexOf("#EXTINF:", start - 1);
|
||||
if (start != -1) {
|
||||
totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
|
||||
} else {
|
||||
logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return numberOfFiles;
|
||||
}
|
||||
|
||||
public void createPlayList() {
|
||||
String m3u8File = readCamerasPlaylist(cameraIndex);
|
||||
if (m3u8File == "") {
|
||||
return;
|
||||
}
|
||||
int numberOfSegments = howManySegments(m3u8File);
|
||||
logger.debug("Using {} segmented files to make up a poll period.", numberOfSegments);
|
||||
m3u8File = keepLast(m3u8File, numberOfSegments);
|
||||
m3u8File = m3u8File.replace("ipcamera", cameraIndex + "ipcamera"); // add index so we can then fetch output path
|
||||
if (entries > numberOfSegments * 3) {
|
||||
playingNow = removeFromStart(playingNow, entries - (numberOfSegments * 3));
|
||||
}
|
||||
playingNow = playingNow + "#EXT-X-DISCONTINUITY\n" + m3u8File;
|
||||
playList = "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:5\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
|
||||
+ discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n" + playingNow;
|
||||
}
|
||||
|
||||
private IpCameraGroupHandler getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void startStreamServer(boolean start) {
|
||||
if (!start) {
|
||||
serversLoopGroup.shutdownGracefully(8, 8, TimeUnit.SECONDS);
|
||||
serverBootstrap = null;
|
||||
} else {
|
||||
if (serverBootstrap == null) {
|
||||
try {
|
||||
serversLoopGroup = new NioEventLoopGroup();
|
||||
serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(serversLoopGroup);
|
||||
serverBootstrap.channel(NioServerSocketChannel.class);
|
||||
// IP "0.0.0.0" will bind the server to all network connections//
|
||||
serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", serverPort));
|
||||
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 25, 0));
|
||||
socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
|
||||
socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
|
||||
socketChannel.pipeline().addLast("streamServerHandler",
|
||||
new StreamServerGroupHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
serverFuture = serverBootstrap.bind().sync();
|
||||
serverFuture.await(4000);
|
||||
logger.info("IpCamera file server for a group of cameras has started on port {} for all NIC's.",
|
||||
serverPort);
|
||||
updateState(CHANNEL_MJPEG_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.mjpeg"));
|
||||
updateState(CHANNEL_HLS_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.jpg"));
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Exception occured when starting the streaming server. Try changing the serverPort to another number.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addCamera(String UniqueID) {
|
||||
if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
|
||||
for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
|
||||
if (handler.getThing().getUID().getId().equals(UniqueID)) {
|
||||
if (!cameraOrder.contains(handler)) {
|
||||
logger.info("Adding {} to a camera group.", UniqueID);
|
||||
if (hlsTurnedOn) {
|
||||
logger.info("Starting HLS for the new camera.");
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
}
|
||||
cameraOrder.add(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event based. This is called as each camera comes online after the group handler is registered.
|
||||
public void cameraOnline(String uid) {
|
||||
logger.debug("New camera {} came online, checking if part of this group", uid);
|
||||
if (groupConfig.getFirstCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getSecondCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getThirdCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getForthCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Event based. This is called as each camera comes online after the group handler is registered.
|
||||
public void cameraOffline(IpCameraHandler handle) {
|
||||
if (cameraOrder.remove(handle)) {
|
||||
logger.info("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
|
||||
}
|
||||
}
|
||||
|
||||
boolean addIfOnline(String UniqueID) {
|
||||
if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
|
||||
addCamera(UniqueID);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void createCameraOrder() {
|
||||
addIfOnline(groupConfig.getFirstCamera());
|
||||
addIfOnline(groupConfig.getSecondCamera());
|
||||
if (!groupConfig.getThirdCamera().isEmpty()) {
|
||||
addIfOnline(groupConfig.getThirdCamera());
|
||||
}
|
||||
if (!groupConfig.getForthCamera().isEmpty()) {
|
||||
addIfOnline(groupConfig.getForthCamera());
|
||||
}
|
||||
// Cameras can now send events of when they go on and offline.
|
||||
groupTracker.listOfGroupHandlers.add(this);
|
||||
}
|
||||
|
||||
int checkForMotion(int nextCamerasIndex) {
|
||||
int checked = 0;
|
||||
for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
|
||||
if (cameraOrder.get(index).motionDetected) {
|
||||
return index;
|
||||
}
|
||||
if (++index >= cameraOrder.size()) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
return nextCamerasIndex;
|
||||
}
|
||||
|
||||
void pollCameraGroup() {
|
||||
if (cameraOrder.isEmpty()) {
|
||||
createCameraOrder();
|
||||
}
|
||||
if (++cameraIndex >= cameraOrder.size()) {
|
||||
cameraIndex = 0;
|
||||
}
|
||||
if (motionChangesOrder) {
|
||||
cameraIndex = checkForMotion(cameraIndex);
|
||||
}
|
||||
if (hlsTurnedOn) {
|
||||
discontinuitySequence++;
|
||||
createPlayList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (!(command instanceof RefreshType)) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_START_STREAM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hlsTurnedOn = true;
|
||||
for (IpCameraHandler handler : cameraOrder) {
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
}
|
||||
} else {
|
||||
// TODO: Do we turn all controls OFF or do we remember the state before we turned them all on?
|
||||
hlsTurnedOn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
serverPort = groupConfig.getServerPort();
|
||||
pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
|
||||
pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
|
||||
motionChangesOrder = groupConfig.getMotionChangesOrder();
|
||||
|
||||
if (serverPort == -1) {
|
||||
logger.warn("The serverPort = -1 which disables a lot of features. See readme for more info.");
|
||||
} else if (serverPort < 1025) {
|
||||
logger.warn("The serverPort is <= 1024 and may cause permission errors under Linux, try a higher port.");
|
||||
}
|
||||
if (groupConfig.getServerPort() > 0) {
|
||||
startStreamServer(true);
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
pollCameraGroupJob = pollCameraGroup.scheduleAtFixedRate(this::pollCameraGroup, 10000,
|
||||
groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
startStreamServer(false);
|
||||
groupTracker.listOfGroupHandlers.remove(this);
|
||||
if (pollCameraGroupJob != null) {
|
||||
pollCameraGroupJob.cancel(true);
|
||||
pollCameraGroupJob = null;
|
||||
}
|
||||
cameraOrder.clear();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.onvif;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link OnvifCodec} is used by Netty to decode Onvif traffic into message Strings.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OnvifCodec extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private String incomingMessage = "";
|
||||
private OnvifConnection onvifConnection;
|
||||
|
||||
OnvifCodec(OnvifConnection onvifConnection) {
|
||||
this.onvifConnection = onvifConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
incomingMessage += content.content().toString(CharsetUtil.UTF_8);
|
||||
}
|
||||
if (msg instanceof LastHttpContent) {
|
||||
onvifConnection.processReply(incomingMessage);
|
||||
ctx.close();
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
|
||||
if (ctx == null || cause == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Exception on ONVIF connection: {}", cause.getMessage());
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.onvif;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OnvifConnection {
|
||||
public static enum RequestType {
|
||||
AbsoluteMove,
|
||||
AddPTZConfiguration,
|
||||
ContinuousMoveLeft,
|
||||
ContinuousMoveRight,
|
||||
ContinuousMoveUp,
|
||||
ContinuousMoveDown,
|
||||
Stop,
|
||||
ContinuousMoveIn,
|
||||
ContinuousMoveOut,
|
||||
CreatePullPointSubscription,
|
||||
GetCapabilities,
|
||||
GetDeviceInformation,
|
||||
GetProfiles,
|
||||
GetServiceCapabilities,
|
||||
GetSnapshotUri,
|
||||
GetStreamUri,
|
||||
GetSystemDateAndTime,
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
PullMessages,
|
||||
GetEventProperties,
|
||||
RelativeMoveLeft,
|
||||
RelativeMoveRight,
|
||||
RelativeMoveUp,
|
||||
RelativeMoveDown,
|
||||
RelativeMoveIn,
|
||||
RelativeMoveOut,
|
||||
Renew,
|
||||
GetConfigurations,
|
||||
GetConfigurationOptions,
|
||||
GetConfiguration,
|
||||
SetConfiguration,
|
||||
GetNodes,
|
||||
GetStatus,
|
||||
GotoPreset,
|
||||
GetPresets
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable Bootstrap bootstrap;
|
||||
private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
|
||||
private String ipAddress = "";
|
||||
private String user = "";
|
||||
private String password = "";
|
||||
private int onvifPort = 80;
|
||||
private String deviceXAddr = "/onvif/device_service";
|
||||
private String eventXAddr = "/onvif/device_service";
|
||||
private String mediaXAddr = "/onvif/device_service";
|
||||
@SuppressWarnings("unused")
|
||||
private String imagingXAddr = "/onvif/device_service";
|
||||
private String ptzXAddr = "/onvif/ptz_service";
|
||||
private String subscriptionXAddr = "/onvif/device_service";
|
||||
private boolean isConnected = false;
|
||||
private int mediaProfileIndex = 0;
|
||||
private String snapshotUri = "";
|
||||
private String rtspUri = "";
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private boolean usingEvents = false;
|
||||
|
||||
// These hold the cameras PTZ position in the range that the camera uses, ie
|
||||
// mine is -1 to +1
|
||||
private Float panRangeMin = -1.0f;
|
||||
private Float panRangeMax = 1.0f;
|
||||
private Float tiltRangeMin = -1.0f;
|
||||
private Float tiltRangeMax = 1.0f;
|
||||
private Float zoomMin = 0.0f;
|
||||
private Float zoomMax = 1.0f;
|
||||
// These hold the PTZ values for updating Openhabs controls in 0-100 range
|
||||
private Float currentPanPercentage = 0.0f;
|
||||
private Float currentTiltPercentage = 0.0f;
|
||||
private Float currentZoomPercentage = 0.0f;
|
||||
private Float currentPanCamValue = 0.0f;
|
||||
private Float currentTiltCamValue = 0.0f;
|
||||
private Float currentZoomCamValue = 0.0f;
|
||||
private String ptzNodeToken = "000";
|
||||
private String ptzConfigToken = "000";
|
||||
private int presetTokenIndex = 0;
|
||||
private LinkedList<String> presetTokens = new LinkedList<>();
|
||||
private LinkedList<String> mediaProfileTokens = new LinkedList<>();
|
||||
private boolean ptzDevice = true;
|
||||
|
||||
public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
if (!ipAddress.isEmpty()) {
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
getIPandPortFromUrl(ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
String getXml(RequestType requestType) {
|
||||
switch (requestType) {
|
||||
case AbsoluteMove:
|
||||
return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
|
||||
+ currentPanCamValue + "\" y=\"" + currentTiltCamValue
|
||||
+ "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
|
||||
+ "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
|
||||
+ "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
|
||||
+ "</Zoom>\n" + "</Position>\n"
|
||||
+ "<Speed><PanTilt x=\"0.1\" y=\"0.1\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace\"></PanTilt><Zoom x=\"1.0\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace\"></Zoom>\n"
|
||||
+ "</Speed></AbsoluteMove>";
|
||||
case AddPTZConfiguration: // not tested to work yet
|
||||
return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
|
||||
+ ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
|
||||
case ContinuousMoveLeft:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveRight:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveUp:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveDown:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case Stop:
|
||||
return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
|
||||
case ContinuousMoveIn:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveOut:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case CreatePullPointSubscription:
|
||||
return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
|
||||
case GetCapabilities:
|
||||
return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
|
||||
|
||||
case GetDeviceInformation:
|
||||
return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
|
||||
case GetProfiles:
|
||||
return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
|
||||
case GetServiceCapabilities:
|
||||
return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
|
||||
case GetSnapshotUri:
|
||||
return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
|
||||
case GetStreamUri:
|
||||
return "<GetStreamUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><StreamSetup><Stream xmlns=\"http://www.onvif.org/ver10/schema\">RTP-Unicast</Stream><Transport xmlns=\"http://www.onvif.org/ver10/schema\"><Protocol>RTSP</Protocol></Transport></StreamSetup><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
|
||||
case GetSystemDateAndTime:
|
||||
return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
|
||||
case Subscribe:
|
||||
return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
|
||||
+ ipCameraHandler.hostIp + ":" + ipCameraHandler.cameraConfig.getServerPort()
|
||||
+ "/OnvifEvent</Address></ConsumerReference></Subscribe>";
|
||||
case Unsubscribe:
|
||||
return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
|
||||
case PullMessages:
|
||||
return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
|
||||
case GetEventProperties:
|
||||
return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
|
||||
case RelativeMoveLeft:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveRight:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveUp:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveDown:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveIn:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveOut:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case Renew:
|
||||
return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT1M</TerminationTime></Renew>";
|
||||
case GetConfigurations:
|
||||
return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
|
||||
case GetConfigurationOptions:
|
||||
return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
|
||||
+ ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
|
||||
case GetConfiguration:
|
||||
return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
|
||||
+ ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
|
||||
case SetConfiguration:// not tested to work yet
|
||||
return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
|
||||
+ ptzNodeToken
|
||||
+ "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
|
||||
case GetNodes:
|
||||
return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
|
||||
case GetStatus:
|
||||
return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
|
||||
case GotoPreset:
|
||||
return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
|
||||
+ presetTokens.get(presetTokenIndex)
|
||||
+ "</PresetToken><Speed><PanTilt x=\"0.0\" y=\"0.0\" space=\"\"></PanTilt><Zoom x=\"0.0\" space=\"\"></Zoom></Speed></GotoPreset>";
|
||||
case GetPresets:
|
||||
return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
|
||||
}
|
||||
return "notfound";
|
||||
}
|
||||
|
||||
public void processReply(String message) {
|
||||
logger.trace("Onvif reply is:{}", message);
|
||||
if (message.contains("PullMessagesResponse")) {
|
||||
eventRecieved(message);
|
||||
} else if (message.contains("RenewResponse")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
|
||||
} else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
|
||||
isConnected = true;
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetCapabilities, deviceXAddr));
|
||||
parseDateAndTime(message);
|
||||
logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
|
||||
} else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
|
||||
parseXAddr(message);
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetProfiles, mediaXAddr));
|
||||
} else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
|
||||
parseProfiles(message);
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetSnapshotUri, mediaXAddr));
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetStreamUri, mediaXAddr));
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.GetNodes);
|
||||
}
|
||||
if (usingEvents) {// stops API cameras from getting sent ONVIF events.
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetEventProperties, eventXAddr));
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetServiceCapabilities, eventXAddr));
|
||||
}
|
||||
} else if (message.contains("GetServiceCapabilitiesResponse")) {
|
||||
if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.Subscribe, eventXAddr));
|
||||
}
|
||||
} else if (message.contains("GetEventPropertiesResponse")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr));
|
||||
} else if (message.contains("SubscribeResponse")) {
|
||||
logger.info("Onvif Subscribe appears to be working for Alarms/Events.");
|
||||
} else if (message.contains("CreatePullPointSubscriptionResponse")) {
|
||||
subscriptionXAddr = removeIPfromUrl(Helper.fetchXML(message, "SubscriptionReference>", "Address>"));
|
||||
logger.debug("subscriptionXAddr={}", subscriptionXAddr);
|
||||
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
|
||||
} else if (message.contains("GetStatusResponse")) {
|
||||
processPTZLocation(message);
|
||||
} else if (message.contains("GetPresetsResponse")) {
|
||||
presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
|
||||
} else if (message.contains("GetConfigurationsResponse")) {
|
||||
sendPTZRequest(RequestType.GetPresets);
|
||||
ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
|
||||
logger.debug("ptzConfigToken={}", ptzConfigToken);
|
||||
sendPTZRequest(RequestType.GetConfigurationOptions);
|
||||
} else if (message.contains("GetNodesResponse")) {
|
||||
sendPTZRequest(RequestType.GetStatus);
|
||||
ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
|
||||
logger.debug("ptzNodeToken={}", ptzNodeToken);
|
||||
sendPTZRequest(RequestType.GetConfigurations);
|
||||
} else if (message.contains("GetDeviceInformationResponse")) {
|
||||
logger.debug("GetDeviceInformationResponse recieved");
|
||||
} else if (message.contains("GetSnapshotUriResponse")) {
|
||||
snapshotUri = removeIPfromUrl(Helper.fetchXML(message, ":MediaUri", ":Uri"));
|
||||
logger.debug("GetSnapshotUri:{}", snapshotUri);
|
||||
if (ipCameraHandler.snapshotUri.isEmpty()) {
|
||||
ipCameraHandler.snapshotUri = snapshotUri;
|
||||
}
|
||||
} else if (message.contains("GetStreamUriResponse")) {
|
||||
rtspUri = Helper.fetchXML(message, ":MediaUri", ":Uri>");
|
||||
logger.debug("GetStreamUri:{}", rtspUri);
|
||||
if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
|
||||
ipCameraHandler.rtspUri = rtspUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequest requestBuilder(RequestType requestType, String xAddr) {
|
||||
logger.trace("Sending ONVIF request:{}", requestType);
|
||||
String security = "";
|
||||
String extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
|
||||
String headerTo = "";
|
||||
String getXmlCache = getXml(requestType);
|
||||
if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
|
||||
|| requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
|
||||
headerTo = "<a:To s:mustUnderstand=\"1\">http://" + ipAddress + xAddr + "</a:To>";
|
||||
}
|
||||
if (!password.isEmpty()) {
|
||||
String nonce = createNonce();
|
||||
String dateTime = getUTCdateTime();
|
||||
String digest = createDigest(nonce, dateTime);
|
||||
security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
|
||||
+ user
|
||||
+ "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
|
||||
+ digest
|
||||
+ "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
|
||||
+ encodeBase64(nonce)
|
||||
+ "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
|
||||
+ dateTime + "</Created></UsernameToken></Security>";
|
||||
}
|
||||
String headers = "<s:Header>" + security + headerTo + "</s:Header>";
|
||||
|
||||
if (requestType.equals(RequestType.GetSystemDateAndTime)) {
|
||||
extraEnvelope = "";
|
||||
headers = "";
|
||||
}
|
||||
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"), xAddr);
|
||||
request.headers().add("Content-Type", "application/soap+xml");
|
||||
request.headers().add("charset", "utf-8");
|
||||
if (onvifPort != 80) {
|
||||
request.headers().set("Host", ipAddress + ":" + onvifPort);
|
||||
} else {
|
||||
request.headers().set("Host", ipAddress);
|
||||
}
|
||||
request.headers().set("Connection", HttpHeaderValues.CLOSE);
|
||||
request.headers().set("Accept-Encoding", "gzip, deflate");
|
||||
String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
|
||||
+ headers
|
||||
+ "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
|
||||
+ getXmlCache + "</s:Body></s:Envelope>";
|
||||
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
|
||||
request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
|
||||
request.headers().set("Content-Length", bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
|
||||
* leaving just the
|
||||
* URL.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
String removeIPfromUrl(String url) {
|
||||
int index = url.indexOf(ipAddress);
|
||||
if (index != -1) {// now remove the :port
|
||||
index = url.indexOf("/", index + ipAddress.length());
|
||||
}
|
||||
if (index == -1) {
|
||||
logger.debug("We hit an issue parsing url:{}", url);
|
||||
return "";
|
||||
}
|
||||
return url.substring(index);
|
||||
}
|
||||
|
||||
void parseXAddr(String message) {
|
||||
// Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
|
||||
String temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Device", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
deviceXAddr = temp;
|
||||
logger.debug("deviceXAddr:{}", deviceXAddr);
|
||||
}
|
||||
temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Events", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
subscriptionXAddr = eventXAddr = temp;
|
||||
logger.debug("eventsXAddr:{}", eventXAddr);
|
||||
}
|
||||
temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Media", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
mediaXAddr = temp;
|
||||
logger.debug("mediaXAddr:{}", mediaXAddr);
|
||||
}
|
||||
|
||||
ptzXAddr = removeIPfromUrl(Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr"));
|
||||
if (ptzXAddr.isEmpty()) {
|
||||
ptzDevice = false;
|
||||
logger.trace("Camera must not support PTZ, it failed to give a <tt:PTZ><tt:XAddr>:{}", message);
|
||||
} else {
|
||||
logger.debug("ptzXAddr:{}", ptzXAddr);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseDateAndTime(String message) {
|
||||
String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
|
||||
String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
|
||||
String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
|
||||
logger.debug("Cameras UTC time is : {}:{}:{}", hour, minute, second);
|
||||
String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
|
||||
String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
|
||||
String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
|
||||
logger.debug("Cameras UTC date is : {}-{}-{}", year, month, day);
|
||||
}
|
||||
|
||||
private String getUTCdateTime() {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
||||
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return format.format(new Date());
|
||||
}
|
||||
|
||||
String createNonce() {
|
||||
Random nonce = new Random();
|
||||
return "" + nonce.nextInt();
|
||||
}
|
||||
|
||||
String encodeBase64(String raw) {
|
||||
return Base64.getEncoder().encodeToString(raw.getBytes());
|
||||
}
|
||||
|
||||
String createDigest(String nOnce, String dateTime) {
|
||||
String beforeEncryption = nOnce + dateTime + password;
|
||||
MessageDigest msgDigest;
|
||||
byte[] encryptedRaw = null;
|
||||
try {
|
||||
msgDigest = MessageDigest.getInstance("SHA-1");
|
||||
msgDigest.reset();
|
||||
msgDigest.update(beforeEncryption.getBytes("utf8"));
|
||||
encryptedRaw = msgDigest.digest();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(encryptedRaw);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void sendOnvifRequest(HttpRequest request) {
|
||||
if (bootstrap == null) {
|
||||
bootstrap = new Bootstrap();
|
||||
bootstrap.group(mainEventLoopGroup);
|
||||
bootstrap.channel(NioSocketChannel.class);
|
||||
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
|
||||
bootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
|
||||
bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
|
||||
bootstrap.option(ChannelOption.TCP_NODELAY, true);
|
||||
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 70));
|
||||
socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
|
||||
socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
|
||||
}
|
||||
});
|
||||
}
|
||||
bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(@Nullable ChannelFuture future) {
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
if (future.isDone() && future.isSuccess()) {
|
||||
Channel ch = future.channel();
|
||||
ch.writeAndFlush(request);
|
||||
} else { // an error occured
|
||||
logger.debug("Camera is not reachable on ONVIF port:{} or the port may be wrong.", onvifPort);
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
OnvifConnection getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
void getIPandPortFromUrl(String url) {
|
||||
int beginIndex = url.indexOf(":");
|
||||
int endIndex = url.indexOf("/", beginIndex);
|
||||
if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
|
||||
ipAddress = url.substring(0, beginIndex);
|
||||
onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
|
||||
} else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
|
||||
ipAddress = url.substring(0, beginIndex);
|
||||
onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
|
||||
} else {// 192.168.1.1
|
||||
ipAddress = url;
|
||||
logger.debug("No Onvif Port found when parsing:{}", url);
|
||||
}
|
||||
}
|
||||
|
||||
public void gotoPreset(int index) {
|
||||
if (ptzDevice) {
|
||||
if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
|
||||
if (presetTokens.isEmpty()) {
|
||||
logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
|
||||
sendPTZRequest(RequestType.GetPresets);
|
||||
} else {
|
||||
presetTokenIndex = index - 1;
|
||||
sendPTZRequest(RequestType.GotoPreset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void eventRecieved(String eventMessage) {
|
||||
String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
|
||||
String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
|
||||
String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
|
||||
if (!topic.isEmpty()) {
|
||||
logger.debug("Onvif Event Topic:{}, Data:{}, Value:{}", topic, dataName, dataValue);
|
||||
}
|
||||
switch (topic) {
|
||||
case "RuleEngine/CellMotionDetector/Motion":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/MotionAlarm":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "AudioAnalytics/Audio/DetectedSound":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/FieldDetector/ObjectsInside":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/LineDetector/Crossed":
|
||||
if (dataName.equals("ObjectId")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/TamperDetector/Tamper":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "Device/HardwareFailure/StorageFailure":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooDark/AnalyticsService":
|
||||
case "VideoSource/ImageTooDark/ImagingService":
|
||||
case "VideoSource/ImageTooDark/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/GlobalSceneChange/AnalyticsService":
|
||||
case "VideoSource/GlobalSceneChange/ImagingService":
|
||||
case "VideoSource/GlobalSceneChange/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooBright/AnalyticsService":
|
||||
case "VideoSource/ImageTooBright/ImagingService":
|
||||
case "VideoSource/ImageTooBright/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooBlurry/AnalyticsService":
|
||||
case "VideoSource/ImageTooBlurry/ImagingService":
|
||||
case "VideoSource/ImageTooBlurry/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr));
|
||||
}
|
||||
|
||||
public boolean supportsPTZ() {
|
||||
return ptzDevice;
|
||||
}
|
||||
|
||||
public void getStatus() {
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.GetStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public Float getAbsolutePan() {
|
||||
return currentPanPercentage;
|
||||
}
|
||||
|
||||
public Float getAbsoluteTilt() {
|
||||
return currentTiltPercentage;
|
||||
}
|
||||
|
||||
public Float getAbsoluteZoom() {
|
||||
return currentZoomPercentage;
|
||||
}
|
||||
|
||||
public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentPanPercentage = panValue;
|
||||
currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentTiltPercentage = tiltValue;
|
||||
currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentZoomPercentage = zoomValue;
|
||||
currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.AbsoluteMove);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedMediaProfile(int mediaProfileIndex) {
|
||||
this.mediaProfileIndex = mediaProfileIndex;
|
||||
}
|
||||
|
||||
LinkedList<String> listOfResults(String message, String heading, String key) {
|
||||
LinkedList<String> results = new LinkedList<String>();
|
||||
String temp = "";
|
||||
for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
|
||||
startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
|
||||
if (startLookingFromIndex >= 0) {
|
||||
temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
|
||||
if (!temp.isEmpty()) {
|
||||
logger.trace("String was found:{}", temp);
|
||||
results.add(temp);
|
||||
++startLookingFromIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void parseProfiles(String message) {
|
||||
mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
|
||||
if (mediaProfileIndex >= mediaProfileTokens.size()) {
|
||||
logger.warn(
|
||||
"You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
|
||||
mediaProfileIndex, mediaProfileTokens.size());
|
||||
mediaProfileIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void processPTZLocation(String result) {
|
||||
logger.debug("Processing new PTZ location now");
|
||||
|
||||
int beginIndex = result.indexOf("x=\"");
|
||||
int endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
|
||||
* 100;
|
||||
logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
|
||||
currentPanCamValue);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
|
||||
return;
|
||||
}
|
||||
|
||||
beginIndex = result.indexOf("y=\"");
|
||||
endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
|
||||
* 100;
|
||||
logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
|
||||
currentTiltCamValue);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
beginIndex = result.lastIndexOf("x=\"");
|
||||
endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
|
||||
logger.debug("Zoom is updating to:{} and the cam value is {}", Math.round(currentZoomPercentage),
|
||||
currentZoomCamValue);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPTZRequest(RequestType requestType) {
|
||||
sendOnvifRequest(requestBuilder(requestType, ptzXAddr));
|
||||
}
|
||||
|
||||
public void sendEventRequest(RequestType requestType) {
|
||||
sendOnvifRequest(requestBuilder(requestType, eventXAddr));
|
||||
}
|
||||
|
||||
public void connect(boolean useEvents) {
|
||||
if (!isConnected) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetSystemDateAndTime, deviceXAddr));
|
||||
usingEvents = useEvents;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (usingEvents && isConnected) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
isConnected = false;
|
||||
presetTokens.clear();
|
||||
mediaProfileTokens.clear();
|
||||
if (!mainEventLoopGroup.isShutdown()) {
|
||||
try {
|
||||
mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
logger.info("Onvif was not shutdown correctly due to being interrupted");
|
||||
} finally {
|
||||
mainEventLoopGroup = new NioEventLoopGroup();
|
||||
bootstrap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.onvif;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraDiscoveryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.channel.socket.InternetProtocolFamily;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
/**
|
||||
* The {@link OnvifDiscovery} is responsible for finding cameras that are Onvif using UDP multicast.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OnvifDiscovery {
|
||||
private IpCameraDiscoveryService ipCameraDiscoveryService;
|
||||
private final Logger logger = LoggerFactory.getLogger(OnvifDiscovery.class);
|
||||
public ArrayList<DatagramPacket> listOfReplys = new ArrayList<DatagramPacket>(10);
|
||||
|
||||
public OnvifDiscovery(IpCameraDiscoveryService ipCameraDiscoveryService) {
|
||||
this.ipCameraDiscoveryService = ipCameraDiscoveryService;
|
||||
}
|
||||
|
||||
public @Nullable NetworkInterface getLocalNIF() {
|
||||
try {
|
||||
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
|
||||
.hasMoreElements();) {
|
||||
NetworkInterface networkInterface = enumNetworks.nextElement();
|
||||
for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
|
||||
.hasMoreElements();) {
|
||||
InetAddress inetAddress = enumIpAddr.nextElement();
|
||||
if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
|
||||
&& inetAddress.isSiteLocalAddress()) {
|
||||
return networkInterface;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void searchReply(String url, String xml) {
|
||||
String ipAddress = "";
|
||||
String temp = url;
|
||||
BigDecimal onvifPort = new BigDecimal(80);
|
||||
|
||||
logger.info("Camera found at xAddr:{}", url);
|
||||
int endIndex = temp.indexOf(" ");// Some xAddr have two urls with a space in between.
|
||||
if (endIndex > 0) {
|
||||
temp = temp.substring(0, endIndex);// Use only the first url from now on.
|
||||
}
|
||||
|
||||
int beginIndex = temp.indexOf(":") + 3;// add 3 to ignore the :// after http.
|
||||
int secondIndex = temp.indexOf(":", beginIndex); // find second :
|
||||
endIndex = temp.indexOf("/", beginIndex);
|
||||
if (secondIndex > beginIndex && endIndex > secondIndex) {// http://192.168.0.1:8080/onvif/device_service
|
||||
ipAddress = temp.substring(beginIndex, secondIndex);
|
||||
onvifPort = new BigDecimal(temp.substring(secondIndex + 1, endIndex));
|
||||
} else {// // http://192.168.0.1/onvif/device_service
|
||||
ipAddress = temp.substring(beginIndex, endIndex);
|
||||
}
|
||||
String brand = checkForBrand(xml);
|
||||
if (brand.equals("onvif")) {
|
||||
try {
|
||||
brand = getBrandFromLoginPage(ipAddress);
|
||||
} catch (IOException e) {
|
||||
brand = "onvif";
|
||||
}
|
||||
}
|
||||
ipCameraDiscoveryService.newCameraFound(brand, ipAddress, onvifPort.intValue());
|
||||
}
|
||||
|
||||
void processCameraReplys() {
|
||||
for (DatagramPacket packet : listOfReplys) {
|
||||
logger.trace("Device replied to discovery with:{}", packet.toString());
|
||||
String xml = packet.content().toString(CharsetUtil.UTF_8);
|
||||
String xAddr = Helper.fetchXML(xml, "", "<d:XAddrs>");
|
||||
if (!xAddr.equals("")) {
|
||||
searchReply(xAddr, xml);
|
||||
} else if (xml.contains("onvif")) {
|
||||
logger.info("Possible ONVIF camera found at:{}", packet.sender().getHostString());
|
||||
ipCameraDiscoveryService.newCameraFound("onvif", packet.sender().getHostString(), 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String checkForBrand(String response) {
|
||||
if (response.toLowerCase().contains("amcrest")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("dahua")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("foscam")) {
|
||||
return "foscam";
|
||||
} else if (response.toLowerCase().contains("hikvision")) {
|
||||
return "hikvision";
|
||||
} else if (response.toLowerCase().contains("instar")) {
|
||||
return "instar";
|
||||
} else if (response.toLowerCase().contains("doorbird")) {
|
||||
return "doorbird";
|
||||
} else if (response.toLowerCase().contains("ipc-")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("dh-sd")) {
|
||||
return "dahua";
|
||||
}
|
||||
return "onvif";
|
||||
}
|
||||
|
||||
public String getBrandFromLoginPage(String hostname) throws IOException {
|
||||
URL url = new URL("http://" + hostname);
|
||||
String brand = "onvif";
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(1000);
|
||||
connection.setReadTimeout(2000);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setRequestMethod("GET");
|
||||
try {
|
||||
connection.connect();
|
||||
BufferedReader reply = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String response = "";
|
||||
String temp;
|
||||
while ((temp = reply.readLine()) != null) {
|
||||
response += temp;
|
||||
}
|
||||
reply.close();
|
||||
logger.trace("Cameras Login page is:{}", response);
|
||||
brand = checkForBrand(response);
|
||||
} catch (MalformedURLException e) {
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
return brand;
|
||||
}
|
||||
|
||||
public void discoverCameras(int port) throws UnknownHostException, InterruptedException {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String xml = "";
|
||||
|
||||
if (port == 3702) {
|
||||
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
|
||||
+ uuid
|
||||
+ "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
|
||||
}
|
||||
ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
|
||||
InetSocketAddress localNetworkAddress = new InetSocketAddress(0);// Listen for replies on all connections.
|
||||
InetSocketAddress multiCastAddress = new InetSocketAddress(InetAddress.getByName("239.255.255.250"), port);
|
||||
DatagramPacket datagramPacket = new DatagramPacket(discoveryProbeMessage, multiCastAddress,
|
||||
localNetworkAddress);
|
||||
NetworkInterface networkInterface = getLocalNIF();
|
||||
DatagramChannel datagramChannel;
|
||||
|
||||
Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
|
||||
.channelFactory(new ChannelFactory<NioDatagramChannel>() {
|
||||
@Override
|
||||
public NioDatagramChannel newChannel() {
|
||||
return new NioDatagramChannel(InternetProtocolFamily.IPv4);
|
||||
}
|
||||
}).handler(new SimpleChannelInboundHandler<DatagramPacket>() {
|
||||
@Override
|
||||
protected void channelRead0(@Nullable ChannelHandlerContext ctx, DatagramPacket msg)
|
||||
throws Exception {
|
||||
msg.retain(1);
|
||||
listOfReplys.add(msg);
|
||||
}
|
||||
}).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
|
||||
.option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
|
||||
.option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
|
||||
|
||||
datagramChannel = (DatagramChannel) bootstrap.bind(localNetworkAddress).sync().channel();
|
||||
datagramChannel.joinGroup(multiCastAddress, networkInterface).sync();
|
||||
ChannelFuture chFuture;
|
||||
if (port == 1900) {
|
||||
String ssdp = "M-SEARCH * HTTP/1.1\n" + "HOST: 239.255.255.250:1900\n" + "MAN: \"ssdp:discover\"\n"
|
||||
+ "MX: 1\n" + "ST: urn:dial-multiscreen-org:service:dial:1\n"
|
||||
+ "USER-AGENT: Microsoft Edge/83.0.478.61 Windows\n" + "\n" + "";
|
||||
ByteBuf ssdpProbeMessage = Unpooled.copiedBuffer(ssdp, 0, ssdp.length(), StandardCharsets.UTF_8);
|
||||
datagramPacket = new DatagramPacket(ssdpProbeMessage, multiCastAddress, localNetworkAddress);
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
} else {
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
}
|
||||
chFuture.awaitUninterruptibly(2000);
|
||||
chFuture = datagramChannel.closeFuture();
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
datagramChannel.close();
|
||||
chFuture.awaitUninterruptibly(6000);
|
||||
processCameraReplys();
|
||||
bootstrap.config().group().shutdownGracefully();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.rtsp;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
|
||||
/**
|
||||
* The {@link NettyRtspHandler} is used to decode RTSP traffic into message Strings.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NettyRtspHandler extends ChannelDuplexHandler {
|
||||
RtspConnection rtspConnection;
|
||||
|
||||
NettyRtspHandler(RtspConnection rtspConnection) {
|
||||
this.rtspConnection = rtspConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (!(msg instanceof LastHttpContent)) {
|
||||
rtspConnection.processMessage(msg);
|
||||
} else {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.ipcamera.internal.rtsp;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.DefaultHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.rtsp.RtspDecoder;
|
||||
import io.netty.handler.codec.rtsp.RtspEncoder;
|
||||
import io.netty.handler.codec.rtsp.RtspHeaderNames;
|
||||
import io.netty.handler.codec.rtsp.RtspMethods;
|
||||
import io.netty.handler.codec.rtsp.RtspVersions;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link RtspConnection} is a WIP and not currently used, but will talk directly to RTSP and collect information
|
||||
* about the camera and streams.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RtspConnection {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable Bootstrap rtspBootstrap;
|
||||
private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
String username, password;
|
||||
|
||||
public RtspConnection(IpCameraHandler ipCameraHandler, String username, String password) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
sendRtspRequest(getRTSPoptions());
|
||||
}
|
||||
|
||||
public void processMessage(Object msg) {
|
||||
logger.info("reply from RTSP is {}", msg);
|
||||
if (msg.toString().contains("DESCRIBE")) {// getRTSPoptions
|
||||
// Public: OPTIONS, DESCRIBE, ANNOUNCE, SETUP, PLAY, RECORD, PAUSE, TEARDOWN, SET_PARAMETER, GET_PARAMETER
|
||||
sendRtspRequest(getRTSPdescribe());
|
||||
} else if (msg.toString().contains("CSeq: 2")) {// getRTSPdescribe
|
||||
// returns this:
|
||||
// RTSP/1.0 200 OK
|
||||
// CSeq: 2
|
||||
// x-Accept-Dynamic-Rate: 1
|
||||
// Content-Base:
|
||||
// rtsp://192.168.xx.xx:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif/
|
||||
// Cache-Control: must-revalidate
|
||||
// Content-Length: 582
|
||||
// Content-Type: application/sdp
|
||||
sendRtspRequest(getRTSPsetup());
|
||||
} else if (msg.toString().contains("CSeq: 3")) {
|
||||
sendRtspRequest(getRTSPplay());
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequest getRTSPoptions() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.OPTIONS,
|
||||
ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "1");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPdescribe() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.DESCRIBE,
|
||||
ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "2");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPsetup() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.SETUP, ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "3");
|
||||
request.headers().add(RtspHeaderNames.TRANSPORT, "RTP/AVP;unicast;client_port=5000-5001");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPplay() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.PLAY, ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "4");
|
||||
// need session to match response from getRTSPsetup()
|
||||
request.headers().add(RtspHeaderNames.SESSION, "12345678");
|
||||
return request;
|
||||
}
|
||||
|
||||
private RtspConnection getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void sendRtspRequest(HttpRequest request) {
|
||||
if (rtspBootstrap == null) {
|
||||
rtspBootstrap = new Bootstrap();
|
||||
rtspBootstrap.group(mainEventLoopGroup);
|
||||
rtspBootstrap.channel(NioSocketChannel.class);
|
||||
rtspBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
rtspBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
|
||||
rtspBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
|
||||
rtspBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
|
||||
rtspBootstrap.option(ChannelOption.TCP_NODELAY, true);
|
||||
rtspBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
|
||||
socketChannel.pipeline().addLast(new RtspDecoder());
|
||||
socketChannel.pipeline().addLast(new RtspEncoder());
|
||||
// Need to update the authhandler to work for multiple use cases, before this works.
|
||||
// socketChannel.pipeline().addLast(new MyNettyAuthHandler(username, password, ipCameraHandler));
|
||||
socketChannel.pipeline().addLast(new NettyRtspHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rtspBootstrap.connect(new InetSocketAddress(ipCameraHandler.cameraConfig.getIp(), 554))
|
||||
.addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(@Nullable ChannelFuture future) {
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
if (future.isDone() && future.isSuccess()) {
|
||||
Channel ch = future.channel();
|
||||
ch.writeAndFlush(request);
|
||||
} else { // an error occured
|
||||
logger.debug("Could not reach cameras rtsp on port 554.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="ipcamera" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>IpCamera Binding</name>
|
||||
<description>This binding helps you to use IP Cameras in Openhab 2.</description>
|
||||
<author>Matthew Skinner</author>
|
||||
</binding:binding>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user