diff --git a/config/alerts/windows.yaml b/config/alerts/windows.yaml index 79d32ba..4d495d3 100644 --- a/config/alerts/windows.yaml +++ b/config/alerts/windows.yaml @@ -22,6 +22,21 @@ attic_window_open: repeat: 30 can_acknowledge: true skip_first: true + notifiers: + - telegram_group + - alexa_all + +garage_door_open: + name: Garagentor geöffnet + message: "Zur Info - Die *Garage* steht noch *offen*!" + done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*." + entity_id: binary_sensor.lumi_garage_door + state: "on" + repeat: + - 10 + - 30 + can_acknowledge: true + skip_first: true notifiers: - telegram_group - alexa_all \ No newline at end of file diff --git a/config/alexa.yaml b/config/alexa.yaml index 6df6d61..cd3cc27 100644 --- a/config/alexa.yaml +++ b/config/alexa.yaml @@ -9,6 +9,7 @@ - light.kuchen_theke - light.esstisch - light.office + - light.onair - switch.livingroom_stimmungslicht - switch.livingroom_music - switch.livingroom_netflix @@ -19,6 +20,8 @@ - switch.desktop_wol - switch.wallboard_display - switch.tplink1 + - switch.osram_plug_01_57b6060a_on_off + - switch.onair_lamp_recording # include_domains: # - switch # exclude_entities: @@ -43,6 +46,9 @@ light.stimmungslicht: name: Stimmungslicht description: Wohnzimmer - Stimmungslicht + light.onair: + name: Studio-Treppe + description: Studio-Treppenlicht switch.livingroom_music: name: Musik description: Wohnzimmer - Musik @@ -70,4 +76,10 @@ switch.tplink1: name: Kamera description: Wohnzimmer Kamera + switch.osram_plug_01_57b6060a_on_off: + name: Ring + description: Studio-Steckdose + switch.onair_lamp_recording: + name: Aufnahme + description: Studio-Treppenlicht Aufnahme-Modus # ACHTUNG: WHITELIST EBENFALLS ERGÄNZEN! \ No newline at end of file diff --git a/config/automations/landroid.yaml b/config/automations/landroid.yaml new file mode 100644 index 0000000..493bb2b --- /dev/null +++ b/config/automations/landroid.yaml @@ -0,0 +1,25 @@ +- alias: Landroid mowing + trigger: + - platform: state + entity_id: sensor.landroid_hans_dieter_status + to: "Mowing" + action: + - service: notify.telegram_group + data_template: + message: "Achtung - *Hans-Dieter* *mäht* jetzt den Rasen!" + - service: notify.alexa_all + data_template: + message: "Achtung - Hans-Dieter maeht jetzt den Rasen!" + +- alias: Landroid home + trigger: + - platform: state + entity_id: sensor.landroid_hans_dieter_status + to: "Home" + action: + - service: notify.telegram_group + data_template: + message: "Zur Info - *Hans-Dieter* ist nun wieder in seiner *Ladestation*." + - service: notify.alexa_all + data_template: + message: "Zur Info - Hans-Dieter ist nun wieder in seiner Ladestation." \ No newline at end of file diff --git a/config/automations/misc.yaml b/config/automations/misc.yaml new file mode 100644 index 0000000..38bc363 --- /dev/null +++ b/config/automations/misc.yaml @@ -0,0 +1,34 @@ +- alias: Instagram Counter - Adjust Brightness + trigger: + platform: state + entity_id: input_number.instagram_counter_brightness + action: + - service: rest_command.instagram_counter_brightness + data_template: + value: "{{ trigger.to_state.state | int }}" + +- alias: onAir Recording On + trigger: + platform: state + entity_id: input_boolean.onair_lamp_recording + to: 'on' + action: + - service: light.turn_on + data: + entity_id: light.onair + rgb_color: [255, 0, 0] + +- alias: onAir Recording Off + trigger: + platform: state + entity_id: input_boolean.onair_lamp_recording + to: 'off' + action: + - service: light.turn_on + data: + entity_id: light.onair + rgb_color: [255, 255, 255] + - service: light.turn_on + data: + entity_id: light.onair + color_temp: 367 diff --git a/config/lights.yaml b/config/lights.yaml index ae1a80c..aa2888b 100644 --- a/config/lights.yaml +++ b/config/lights.yaml @@ -4,4 +4,9 @@ hdmi_priority: 900 - platform: switch name: Stimmungslicht - entity_id: switch.livingroom_stimmungslicht \ No newline at end of file + entity_id: switch.livingroom_stimmungslicht + - platform: group + name: onAir + entities: + - light.innr_gu10_rgb_1 + - light.innr_gu10_rgb_2 \ No newline at end of file diff --git a/config/sensors/social.yaml b/config/sensors/social.yaml index 28ba954..4a6e53e 100644 --- a/config/sensors/social.yaml +++ b/config/sensors/social.yaml @@ -77,20 +77,20 @@ scan_interval: 300 name: instagram_beauty resource: !secret instagram_beauty - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_beauty_follows resource: !secret instagram_beauty - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_beauty_media resource: !secret instagram_beauty - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # franky @@ -98,20 +98,20 @@ scan_interval: 300 name: instagram_franky resource: !secret instagram_franky - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_franky_follows resource: !secret instagram_franky - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_franky_media resource: !secret instagram_franky - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # fb @@ -119,20 +119,20 @@ scan_interval: 300 name: instagram_fb resource: !secret instagram_fb - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_fb_follows resource: !secret instagram_fb - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_fb_media resource: !secret instagram_fb - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # mtb @@ -140,20 +140,20 @@ scan_interval: 300 name: instagram_mtb resource: !secret instagram_mtb - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_mtb_follows resource: !secret instagram_mtb - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_mtb_media resource: !secret instagram_mtb - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # medieval @@ -161,20 +161,20 @@ scan_interval: 300 name: instagram_medieval resource: !secret instagram_medieval - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_medieval_follows resource: !secret instagram_medieval - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_medieval_media resource: !secret instagram_medieval - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # lotte @@ -182,20 +182,20 @@ scan_interval: 300 name: instagram_lotte resource: !secret instagram_lotte - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_lotte_follows resource: !secret instagram_lotte - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_lotte_media resource: !secret instagram_lotte - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # codedwithlove @@ -203,20 +203,20 @@ scan_interval: 300 name: instagram_codedwithlove resource: !secret instagram_codedwithlove - value_template: '{{ value_json.data.counts.followed_by }}' + value_template: '{{ value_json.followers_count }}' unit_of_measurement: Followers force_update: true - platform: rest scan_interval: 900 name: instagram_codedwithlove_follows resource: !secret instagram_codedwithlove - value_template: '{{ value_json.data.counts.follows }}' + value_template: '{{ value_json.follows_count }}' force_update: true - platform: rest scan_interval: 600 name: instagram_codedwithlove_media resource: !secret instagram_codedwithlove - value_template: '{{ value_json.data.counts.media }}' + value_template: '{{ value_json.media_count }}' force_update: true # TikTok diff --git a/config/switches/misc.yaml b/config/switches/misc.yaml index 8768cf5..a3598b3 100644 --- a/config/switches/misc.yaml +++ b/config/switches/misc.yaml @@ -12,4 +12,13 @@ port: !secret nas_ssh_port command_or_param: !secret desktop_mac turn_off: - service: script.dummy \ No newline at end of file + service: script.dummy + + onair_lamp_recording: + value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}" + turn_on: + - service: input_boolean.turn_on + entity_id: input_boolean.onair_lamp_recording + turn_off: + - service: input_boolean.turn_off + entity_id: input_boolean.onair_lamp_recording \ No newline at end of file diff --git a/config/views/lights.yaml b/config/views/lights.yaml index ffe3586..e28efea 100644 --- a/config/views/lights.yaml +++ b/config/views/lights.yaml @@ -15,4 +15,23 @@ cards: name: Ambilight - type: light entity: light.esstisch - name: Esstisch \ No newline at end of file + name: Esstisch + - type: light + entity: light.tint_rgb_gu10_1 + name: Kinderbad + + - type: vertical-stack + title: Studio + cards: + - type: light + entity: light.onair + name: Studio-Treppe + - type: entities + show_header_toggle: false + entities: + - entity: switch.onair_lamp_recording + name: Studio Aufnahme + icon: mdi:camera-rear + - entity: switch.osram_plug_01_57b6060a_on_off + name: Studio Ringlicht + icon: mdi:checkbox-blank-circle-outline \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml index 6c7f535..1b35d65 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -35,7 +35,7 @@ http: ip_ban_enabled: true login_attempts_threshold: 5 use_x_forwarded_for: true - base_url: https://hass.f-brinker.de + base_url: !secret url_base trusted_proxies: - 127.0.0.1 - ::1 @@ -58,7 +58,7 @@ zha: emulated_roku: servers: - name: Home Assistant - listen_port: 8060 + listen_port: !secret roku_port mqtt: broker: !secret mqtt_broker_ip @@ -87,12 +87,6 @@ notify: - name: telegram_fb platform: telegram chat_id: !secret telegram_chat_fb - - platform: command_line - name: alexa_kitchen - command: "/config/alexa_wrapper.sh -d 'Küche'" - - platform: command_line - name: alexa_livingroom - command: "/config/alexa_wrapper.sh -d 'Wohnzimmer'" - platform: command_line name: alexa_all command: "/config/alexa_wrapper.sh -d 'ALL'" @@ -141,10 +135,23 @@ ffmpeg: ffmpeg_bin: /usr/bin/ffmpeg input_datetime: - bedroom_alarm_clock_time: - name: Wecker - has_date: false - has_time: true + bedroom_alarm_clock_time: + name: Wecker + has_date: false + has_time: true + +input_number: + instagram_counter_brightness: + name: Instagram Counter Brightness + initial: 15 + min: 0 + max: 15 + step: 1 + +input_boolean: + onair_lamp_recording: + name: onAir Recording Status + initial: false alarm_control_panel: - platform: manual @@ -152,22 +159,26 @@ alarm_control_panel: code: !secret alarm_code code_arm_required: true delay_time: 20 - pending_time: 30 + arming_time: 30 trigger_time: 120 disarm_after_trigger: false disarmed: trigger_time: 0 armed_home: - pending_time: 0 + arming_time: 0 delay_time: 0 armed_night: - pending_time: 0 + arming_time: 0 delay_time: 0 shell_command: ssh: 'ssh -o "StrictHostKeyChecking=no" -i {{ sshkey }} {{ host }} -l {{ user }} -p {{ port }} {{ command_or_param }}' rest_command: + instagram_counter_brightness: + url: !secret url_instagram_counter_brightness + method: GET + payload: 'value={{ value }}' shinobi_monitorstates: url: "https://{{ host }}/{{ apikey }}/monitorStates/{{ group }}/{{ preset_name }}" @@ -185,10 +196,10 @@ spotify: client_id: !secret spotify_client_id client_secret: !secret spotify_client_secret -tplink: - discovery: false - switch: - - host: !secret tplink_ip +#tplink: +# discovery: false +# switch: +# - host: !secret tplink_ip # External config files alert: !include_dir_merge_named config/alerts/ diff --git a/custom_components/reolink_dev/ReolinkPyPi/__init__.py b/custom_components/reolink_dev/ReolinkPyPi/__init__.py new file mode 100644 index 0000000..7461c90 --- /dev/null +++ b/custom_components/reolink_dev/ReolinkPyPi/__init__.py @@ -0,0 +1 @@ +"""Reolink Camera component for HomeAssistant.""" \ No newline at end of file diff --git a/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..e655c5c Binary files /dev/null and b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/__init__.cpython-37.pyc differ diff --git a/custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-37.pyc b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-37.pyc new file mode 100644 index 0000000..2dde65b Binary files /dev/null and b/custom_components/reolink_dev/ReolinkPyPi/__pycache__/camera.cpython-37.pyc differ diff --git a/custom_components/reolink_dev/ReolinkPyPi/camera.py b/custom_components/reolink_dev/ReolinkPyPi/camera.py new file mode 100644 index 0000000..67d0f2a --- /dev/null +++ b/custom_components/reolink_dev/ReolinkPyPi/camera.py @@ -0,0 +1,278 @@ +""" +Reolink Camera API +""" +import requests +import datetime +import json +import logging + +_LOGGER = logging.getLogger(__name__) + +class ReolinkApi(object): + def __init__(self, ip, channel): + self._url = "http://" + ip + "/cgi-bin/api.cgi" + self._ip = ip + self._channel = channel + self._token = None + self._motion_state = False + self._last_motion = 0 + self._device_info = None + self._motion_state = None + self._ftp_state = None + self._email_state = None + self._ir_state = None + self._rtspport = None + self._rtmpport = None + self._ptzpresets = dict() + + def status(self): + if self._token is None: + return + + param_channel = {"channel": self._channel} + body = [{"cmd": "GetDevInfo", "action":1, "param": param_channel}, + {"cmd": "GetNetPort", "action": 1, "param": param_channel}, + {"cmd": "GetFtp", "action": 1, "param": param_channel}, + {"cmd": "GetEmail", "action": 1, "param": param_channel}, + {"cmd": "GetIrLights", "action": 1, "param": param_channel}, + {"cmd": "GetPtzPreset", "action": 1, "param": param_channel}] + + param = {"token": self._token} + response = self.send(body, param) + + try: + json_data = json.loads(response.text) + except: + _LOGGER.error(f"Error translating response to json") + return + + for data in json_data: + try: + if data["cmd"] == "GetDevInfo": + self._device_info = data + + elif data["cmd"] == "GetNetPort": + self._netport_settings = data + self._rtspport = data["value"]["NetPort"]["rtspPort"] + self._rtmpport = data["value"]["NetPort"]["rtmpPort"] + + elif data["cmd"] == "GetFtp": + self._ftp_settings = data + if (data["value"]["Ftp"]["schedule"]["enable"] == 1): + self._ftp_state = True + else: + self._ftp_state = False + + elif data["cmd"] == "GetEmail": + self._email_settings = data + if (data["value"]["Email"]["schedule"]["enable"] == 1): + self._email_state = True + else: + self._email_state = False + + elif data["cmd"] == "GetIrLights": + self._ir_settings = data + if (data["value"]["IrLights"]["state"] == "Auto"): + self._ir_state = True + else: + self._ir_state = False + + elif data["cmd"] == "GetPtzPreset": + self._ptzpresets_settings = data + for preset in data["value"]["PtzPreset"]: + if int(preset["enable"]) == 1: + preset_name = preset["name"] + preset_id = int(preset["id"]) + self._ptzpresets[preset_name] = preset_id + _LOGGER.debug(f"Got preset {preset_name} with ID {preset_id}") + else: + _LOGGER.debug(f"Preset is not enabled: {preset}") + except: + continue + + @property + def motion_state(self): + body = [{"cmd": "GetMdState", "action": 0, "param":{"channel":self._channel}}] + param = {"token": self._token} + + response = self.send(body, param) + + try: + json_data = json.loads(response.text) + + if json_data is None: + _LOGGER.error(f"Unable to get Motion detection state at IP {self._ip}") + self._motion_state = False + return self._motion_state + + if json_data[0]["value"]["state"] == 1: + self._motion_state = True + self._last_motion = datetime.datetime.now() + else: + self._motion_state = False + except: + self._motion_state = False + + return self._motion_state + + @property + def still_image(self): + response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=True) + response.raw.decode_content = True + return response.raw + + @property + def snapshot(self): + response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=False) + return response.content + + @property + def ftp_state(self): + return self._ftp_state + + @property + def email_state(self): + return self._email_state + + @property + def ir_state(self): + return self._ir_state + + @property + def rtmpport(self): + return self._rtmpport + + @property + def rtspport(self): + return self._rtspport + + @property + def last_motion(self): + return self._last_motion + + @property + def ptzpresets(self): + return self._ptzpresets + + def login(self, username, password): + body = [{"cmd": "Login", "action": 0, "param": {"User": {"userName": username, "password": password}}}] + param = {"cmd": "Login", "token": "null"} + + response = self.send(body, param) + + try: + json_data = json.loads(response.text) + except: + _LOGGER.error(f"Error translating login response to json") + return + + if json_data is not None: + if json_data[0]["code"] == 0: + self._token = json_data[0]["value"]["Token"]["name"] + _LOGGER.info(f"Reolink camera logged in at IP {self._ip}") + else: + _LOGGER.error(f"Failed to login at IP {self._ip}. No token available") + else: + _LOGGER.error(f"Failed to login at IP {self._ip}. Connection error.") + + def logout(self): + body = [{"cmd":"Logout","action":0,"param":{}}] + param = {"cmd": "Logout", "token": self._token} + + self.send(body, param) + + def set_ftp(self, enabled): + self.status() + + if not self._ftp_settings: + _LOGGER.error("Error while fetching current FTP settings") + return + + if enabled == True: + newValue = 1 + else: + newValue = 0 + + body = [{"cmd":"SetFtp","action":0,"param": self._ftp_settings["value"] }] + body[0]["param"]["Ftp"]["schedule"]["enable"] = newValue + + response = self.send(body, {"cmd": "SetFtp", "token": self._token} ) + try: + json_data = json.loads(response.text) + if json_data[0]["value"]["rspCode"] == 200: + return True + else: + return False + except: + _LOGGER.error(f"Error translating FTP response to json") + return False + + def set_email(self, enabled): + self.status() + + if not self._email_settings: + _LOGGER.error("Error while fetching current email settings") + return + + if enabled == True: + newValue = 1 + else: + newValue = 0 + + body = [{"cmd":"SetEmail","action":0,"param": self._email_settings["value"] }] + body[0]["param"]["Email"]["schedule"]["enable"] = newValue + + response = self.send(body, {"cmd": "SetEmail", "token": self._token} ) + try: + json_data = json.loads(response.text) + if json_data[0]["value"]["rspCode"] == 200: + return True + else: + return False + except: + _LOGGER.error(f"Error translating Email response to json") + return False + + def set_ir_lights(self, enabled): + self.status() + + if not self._ir_settings: + _LOGGER.error("Error while fetching current IR light settings") + return + + if enabled == True: + newValue = "Auto" + else: + newValue = "Off" + + body = [{"cmd":"SetIrLights","action":0,"param": self._ir_settings["value"] }] + body[0]["param"]["IrLights"]["state"] = newValue + + response = self.send(body, {"cmd": "SetIrLights", "token": self._token} ) + try: + json_data = json.loads(response.text) + if json_data[0]["value"]["rspCode"] == 200: + return True + else: + return False + except requests.exceptions.RequestException: + _LOGGER.error(f"Error translating IR Lights response to json") + return False + + def send(self, body, param, stream=False): + try: + if (self._token is None and + (body is None or body[0]["cmd"] != "Login")): + _LOGGER.info(f"Reolink camera at IP {self._ip} is not logged in") + return + + if body is None: + response = requests.get(self._url, params=param, stream=stream) + else: + response = requests.post(self._url, data=json.dumps(body), params=param) + + return response + except Exception: + _LOGGER.error(f"Exception while calling Reolink camera API at ip {self._ip}") + return None + diff --git a/custom_components/reolink_dev/__init__.py b/custom_components/reolink_dev/__init__.py new file mode 100644 index 0000000..7461c90 --- /dev/null +++ b/custom_components/reolink_dev/__init__.py @@ -0,0 +1 @@ +"""Reolink Camera component for HomeAssistant.""" \ No newline at end of file diff --git a/custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc b/custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000..61393d3 Binary files /dev/null and b/custom_components/reolink_dev/__pycache__/__init__.cpython-37.pyc differ diff --git a/custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc b/custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc new file mode 100644 index 0000000..4d778bf Binary files /dev/null and b/custom_components/reolink_dev/__pycache__/camera.cpython-37.pyc differ diff --git a/custom_components/reolink_dev/camera.py b/custom_components/reolink_dev/camera.py new file mode 100644 index 0000000..955239d --- /dev/null +++ b/custom_components/reolink_dev/camera.py @@ -0,0 +1,312 @@ +"""This component provides basic support for Reolink IP cameras.""" +import logging +import asyncio +import voluptuous as vol +import datetime + +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM, ENTITY_IMAGE_URL +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv + +from haffmpeg.camera import CameraMjpeg +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from custom_components.reolink_dev.ReolinkPyPi.camera import ReolinkApi + +_LOGGER = logging.getLogger(__name__) + +STATE_MOTION = "motion" +STATE_NO_MOTION = "no_motion" +STATE_IDLE = "idle" + +DEFAULT_NAME = "Reolink Camera" +DEFAULT_STREAM = "main" +DEFAULT_PROTOCOL = "rtmp" +DEFAULT_CHANNEL = 0 +CONF_STREAM = "stream" +CONF_PROTOCOL = "protocol" +CONF_CHANNEL = "channel" +DOMAIN = "camera" +SERVICE_ENABLE_FTP = 'enable_ftp' +SERVICE_DISABLE_FTP = 'disable_ftp' +SERVICE_ENABLE_EMAIL = 'enable_email' +SERVICE_DISABLE_EMAIL = 'disable_email' +SERVICE_ENABLE_IR_LIGHTS = 'enable_ir_lights' +SERVICE_DISABLE_IR_LIGHTS = 'disable_ir_lights' + +DEFAULT_BRAND = 'Reolink' +DOMAIN_DATA = 'reolink_devices' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STREAM, default=DEFAULT_STREAM): vol.In(["main", "sub"]), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(["rtmp", "rtsp"]), + vol.Optional(CONF_CHANNEL, default=DEFAULT_CHANNEL): cv.positive_int, + } +) + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up a Reolink IP Camera.""" + + host = config.get(CONF_HOST) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + stream = config.get(CONF_STREAM) + protocol = config.get(CONF_PROTOCOL) + channel = config.get(CONF_CHANNEL) + name = config.get(CONF_NAME) + + session = ReolinkApi(host, channel) + session.login(username, password) + + async_add_devices([ReolinkCamera(hass, session, host, username, password, stream, protocol, channel, name)], update_before_add=True) + +# Event enable FTP + def handler_enable_ftp(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.enable_ftp_upload() + hass.services.async_register(DOMAIN, SERVICE_ENABLE_FTP, handler_enable_ftp) + +# Event disable FTP + def handler_disable_ftp(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.disable_ftp_upload() + + hass.services.async_register(DOMAIN, SERVICE_DISABLE_FTP, handler_disable_ftp) + +# Event enable email + def handler_enable_email(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.enable_email() + hass.services.async_register(DOMAIN, SERVICE_ENABLE_EMAIL, handler_enable_email) + +# Event disable email + def handler_disable_email(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.disable_email() + + hass.services.async_register(DOMAIN, SERVICE_DISABLE_EMAIL, handler_disable_email) + +# Event enable ir lights + def handler_enable_ir_lights(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.enable_ir_lights() + hass.services.async_register(DOMAIN, SERVICE_ENABLE_IR_LIGHTS, handler_enable_ir_lights) + +# Event disable ir lights + def handler_disable_ir_lights(call): + component = hass.data.get(DOMAIN) + entity = component.get_entity(call.data.get(ATTR_ENTITY_ID)) + + if entity: + entity.disable_ir_lights() + + hass.services.async_register(DOMAIN, SERVICE_DISABLE_IR_LIGHTS, handler_disable_ir_lights) + + +class ReolinkCamera(Camera): + """An implementation of a Reolink IP camera.""" + + def __init__(self, hass, session, host, username, password, stream, protocol, channel, name): + """Initialize a Reolink camera.""" + + super().__init__() + self._host = host + self._username = username + self._password = password + self._stream = stream + self._protocol = protocol + self._channel = channel + self._name = name + self._reolinkSession = session + self._hass = hass + self._manager = self._hass.data[DATA_FFMPEG] + + self._last_update = 0 + self._last_image = None + self._last_motion = 0 + self._ftp_state = None + self._email_state = None + self._ir_state = None + self._ptzpresets = dict() + self._state = STATE_IDLE + + self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.disconnect) + + @property + def state_attributes(self): + """Return the camera state attributes.""" + attrs = {"access_token": self.access_tokens[-1]} + + if self._last_motion: + attrs["last_motion"] = self._last_motion + + if self._last_update: + attrs["last_update"] = self._last_update + + attrs["ftp_enabled"] = self._ftp_state + attrs["email_enabled"] = self._email_state + attrs["ir_lights_enabled"] = self._ir_state + attrs["ptzpresets"] = self._ptzpresets + + return attrs + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def should_poll(self): + """Polling needed for the device status.""" + return True + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def ftp_state(self): + """Camera Motion recording Status.""" + return self._ftp_state + + @property + def email_state(self): + """Camera email Status.""" + return self._email_state + + @property + def ptzpresets(self): + """Camera PTZ presets list.""" + return self._ptzpresets + + async def stream_source(self): + """Return the source of the stream.""" + if self._protocol == "rtsp": + rtspChannel = f"{self._channel+1:02d}" + stream_source = f"rtsp://{self._username}:{self._password}@{self._host}:{self._reolinkSession.rtspport}/h264Preview_{rtspChannel}_{self._stream}" + else: + stream_source = f"rtmp://{self._host}:{self._reolinkSession.rtmpport}/bcs/channel{self._channel}_{self._stream}.bcs?channel={self._channel}&stream=0&user={self._username}&password={self._password}" + + return stream_source + + async def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + stream_source = await self.stream_source() + + stream = CameraMjpeg(self._manager.binary, loop=self._hass.loop) + await stream.open_camera(stream_source) + + try: + stream_reader = await stream.get_reader() + return await async_aiohttp_proxy_stream( + self._hass, + request, + stream_reader, + self._manager.ffmpeg_stream_content_type, + ) + finally: + await stream.close() + + def camera_image(self): + """Return bytes of camera image.""" + return self._reolinkSession.still_image + + async def async_camera_image(self): + """Return a still image response from the camera.""" + return self._reolinkSession.snapshot + + def enable_ftp_upload(self): + """Enable motion ftp recording in camera.""" + if self._reolinkSession.set_ftp(True): + self._ftp_state = True + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + def disable_ftp_upload(self): + """Disable motion ftp recording.""" + if self._reolinkSession.set_ftp(False): + self._ftp_state = False + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + def enable_email(self): + """Enable email motion detection in camera.""" + if self._reolinkSession.set_email(True): + self._email_state = True + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + def disable_email(self): + """Disable email motion detection.""" + if self._reolinkSession.set_email(False): + self._email_state = False + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + def enable_ir_lights(self): + """Enable IR lights.""" + if self._reolinkSession.set_ir_lights(True): + self._ir_state = True + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + def disable_ir_lights(self): + """Disable IR lights.""" + if self._reolinkSession.set_ir_lights(False): + self._ir_state = False + self._hass.states.set(self.entity_id, self.state, self.state_attributes) + + async def update_motion_state(self): + if self._reolinkSession.motion_state == True: + self._state = STATE_MOTION + self._last_motion = self._reolinkSession.last_motion + else: + self._state = STATE_NO_MOTION + + async def update_status(self): + self._reolinkSession.status() + + self._last_update = datetime.datetime.now() + self._ftp_state = self._reolinkSession.ftp_state + self._email_state = self._reolinkSession.email_state + self._ir_state = self._reolinkSession.ir_state + self._ptzpresets = self._reolinkSession.ptzpresets + + def update(self): + """Update the data from the camera.""" + try: + self._hass.loop.create_task(self.update_motion_state()) + + if (self._last_update == 0 or + (datetime.datetime.now() - self._last_update).total_seconds() >= 30): + self._hass.loop.create_task(self.update_status()) + + except Exception as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + def disconnect(self, event): + _LOGGER.info("Disconnecting from Reolink camera") + self._reolinkSession.logout() diff --git a/custom_components/reolink_dev/manifest.json b/custom_components/reolink_dev/manifest.json new file mode 100644 index 0000000..850d570 --- /dev/null +++ b/custom_components/reolink_dev/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "reolink_dev", + "name": "Reolink IP camera", + "documentation": "https://www.example.com", + "dependencies": ["ffmpeg"], + "codeowners": ["@fwestenberg"], + "requirements": ["aiosmtpd==1.2"] + } diff --git a/custom_components/reolink_dev/services.yaml b/custom_components/reolink_dev/services.yaml new file mode 100644 index 0000000..d7762fc --- /dev/null +++ b/custom_components/reolink_dev/services.yaml @@ -0,0 +1,41 @@ +enable_ftp: + description: Enable FTP upload on motion recording. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' + +disable_ftp: + description: Disable FTP upload on motion recording. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' + +enable_email: + description: Enable email functionality on motion detection. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' + +disable_email: + description: Disable email functionality on motion detection. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' + +enable_ir_lights: + description: Enable the infrared lights (nightvision) of the Reolink camera. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' + +disable_ir_lights: + description: Disable the infrared lights (nightvision) of the Reolink camera. + fields: + entity_id: + description: Name of the Reolink camera entity to set. + example: 'camera.frontdoor' \ No newline at end of file diff --git a/packages/landroid.yaml b/packages/landroid.yaml index 1a7b637..553c9bd 100644 --- a/packages/landroid.yaml +++ b/packages/landroid.yaml @@ -212,21 +212,21 @@ input_boolean: # Automations ####################################################### automation: - - id: "landroid_status_notify" - alias: "Landroid Status Notification" - initial_state: true - trigger: - - platform: state - entity_id: sensor.landroid_hans_dieter_status - condition: - - condition: template - value_template: "{{ trigger.to_state.state != trigger.from_state.state }}" - action: - - service: persistent_notification.create - data_template: - title: Lanroid Status - message: "{{ trigger.from_state.state }} -> {{ trigger.to_state.state }} - {{ states('sensor.date_time') }}" - +# - id: "landroid_status_notify" +# alias: "Landroid Status Notification" +# initial_state: true +# trigger: +# - platform: state +# entity_id: sensor.landroid_hans_dieter_status +# condition: +# - condition: template +# value_template: "{{ trigger.to_state.state != trigger.from_state.state }}" +# action: +# - service: persistent_notification.create +# data_template: +# title: Landroid Status +# message: "{{ trigger.from_state.state }} -> {{ trigger.to_state.state }} - {{ states('sensor.date_time') }}" +# - id: "landroid_error_notify" alias: "Landroid Error Notification" initial_state: true @@ -239,7 +239,7 @@ automation: action: - service: persistent_notification.create data_template: - title: Lanroid Status + title: Landroid Status message: "{{ trigger.from_state.state }} -> {{ trigger.to_state.state }} - {{ states('sensor.date_time') }}" # Scripts ########################################################### diff --git a/secrets.yaml.skel b/secrets.yaml.skel index c279a29..e08d169 100644 --- a/secrets.yaml.skel +++ b/secrets.yaml.skel @@ -8,12 +8,18 @@ adbkey: alexa_email: alexa_password: +url_base: + url_chronograf: url_esphome: url_haslave_livingroom: url_haslave_office: +url_instagram_counter_brightness: + +roku_port: + # Zones home_lat: home_long: @@ -56,6 +62,13 @@ cam_livingroom_ip: cam_livingroom_user: cam_livingroom_password: +# Robots +landroid_mail: +landroid_pass: +landroid_ip: +landroid_sn: +landroid_mac: + # Weather openweathermap: @@ -84,17 +97,16 @@ telegram_chat_fb: telegram_chat_group: # Social data -# invite @ https: -# accept @ https: -# wait a while so instagram registers that we are a sandbox user now... -# open and authorize https: +# App https: +# Login & Copy the Instagram URLs https: instagram_beauty: instagram_franky: + instagram_fb: instagram_mtb: +instagram_codedwithlove: instagram_medieval: instagram_lotte: -instagram_codedwithlove: youtube_beauty: youtube_mtb: @@ -103,3 +115,4 @@ bitly_blog_bb: bitly_instagram_bb: bitly_impressum_bb: bitly_youtube_bb: +