From fa79f1208d85cdfafdc14bd629a189073df2e348 Mon Sep 17 00:00:00 2001 From: droop Date: Tue, 17 Oct 2017 00:58:22 -0400 Subject: [PATCH] Added floodlight lights and siren support --- README.rst | 6 +- ring_doorbell/const.py | 6 ++ ring_doorbell/stickup_cam.py | 49 +++++++++++ tests/fixtures/ring_devices.json | 138 +++++++++++++++++++++++++++++++ tests/test_ring.py | 45 ++++++++++ 5 files changed, 243 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3963a8bc..6a0d4678 100644 --- a/README.rst +++ b/README.rst @@ -93,10 +93,14 @@ Playing with the attributes and functions print('Volume: %s' % dev.volume) # play dev test shound - if dev.family == 'chimes' + if dev.family == 'chimes': dev.test_sound(kind = 'ding') dev.test_sound(kind = 'motion') + # turn on lights on floodlight cam + if dev.family == 'stickup_cams' and dev.lights: + dev.lights = 'on' + Showing door bell events ------------------------ diff --git a/ring_doorbell/const.py b/ring_doorbell/const.py index f820539b..98c933e2 100644 --- a/ring_doorbell/const.py +++ b/ring_doorbell/const.py @@ -35,10 +35,12 @@ HEALTH_DOORBELL_ENDPOINT = DOORBELLS_ENDPOINT + '/health' HEALTH_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/health' +LIGHTS_ENDPOINT = DOORBELLS_ENDPOINT + '/floodlight_light_{1}' LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots' LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod' NEW_SESSION_ENDPOINT = '/clients_api/session' RINGTONES_ENDPOINT = '/ringtones' +SIREN_ENDPOINT = DOORBELLS_ENDPOINT + '/siren_{1}' TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + '/play_sound' URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history' URL_RECORDING = '/clients_api/dings/{0}/recording' @@ -60,12 +62,16 @@ 1: 'Digital', 2: 'Not Present'} +SIREN_DURATION_MIN = 0 +SIREN_DURATION_MAX = 120 + # error strings MSG_BOOLEAN_REQUIRED = "Boolean value is required." MSG_EXISTING_TYPE = "Integer value where {0}.".format(DOORBELL_EXISTING_TYPE) MSG_GENERIC_FAIL = 'Sorry.. Something went wrong...' FILE_EXISTS = 'The file {0} already exists.' MSG_VOL_OUTBOUND = 'Must be within the {0}-{1}.' +MSG_ALLOWED_VALUES = 'Only the following values are allowed: {0}.' # structure acquired from reverse engineering to create auth token POST_DATA = { diff --git a/ring_doorbell/stickup_cam.py b/ring_doorbell/stickup_cam.py index 7ad45c5b..0f86bb54 100644 --- a/ring_doorbell/stickup_cam.py +++ b/ring_doorbell/stickup_cam.py @@ -4,6 +4,9 @@ import logging from ring_doorbell import RingDoorBell +from ring_doorbell.const import ( + API_URI, LIGHTS_ENDPOINT, MSG_ALLOWED_VALUES, MSG_VOL_OUTBOUND, + SIREN_DURATION_MIN, SIREN_DURATION_MAX, SIREN_ENDPOINT) _LOGGER = logging.getLogger(__name__) @@ -15,3 +18,49 @@ class RingStickUpCam(RingDoorBell): def family(self): """Return Ring device family type.""" return 'stickup_cams' + + @property + def lights(self): + """Return lights status.""" + return self._attrs.get('led_status') + + @lights.setter + def lights(self, state): + """Control the lights.""" + values = ['on', 'off'] + if state not in values: + _LOGGER.error("%s", MSG_ALLOWED_VALUES.format(', '.join(values))) + return False + + url = API_URI + LIGHTS_ENDPOINT.format(self.account_id, state) + self._ring.query(url, method='PUT') + self.update() + return True + + @property + def siren(self): + """Return siren status.""" + if self._attrs.get('siren_status'): + return self._attrs.get('siren_status').get('seconds_remaining') + return None + + @siren.setter + def siren(self, duration): + """Control the siren.""" + if not ((isinstance(duration, int)) and + (duration >= SIREN_DURATION_MIN and + duration <= SIREN_DURATION_MAX)): + _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(SIREN_DURATION_MIN, + SIREN_DURATION_MAX)) + return False + + if duration > 0: + state = 'on' + params = {'duration': duration} + else: + state = 'off' + params = {} + url = API_URI + SIREN_ENDPOINT.format(self.account_id, state) + self._ring.query(url, extra_params=params, method='PUT') + self.update() + return True diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index f7f723e3..d5d16ff4 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -124,5 +124,143 @@ "high"]}, "subscribed": true, "subscribed_motions": true, + "time_zone": "America/New_York"}], + "stickup_cams": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 100, + "description": "Front", + "device_id": "aacdef123", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "off", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 0}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, "time_zone": "America/New_York"}] } diff --git a/tests/test_ring.py b/tests/test_ring.py index 3203e832..5881d877 100644 --- a/tests/test_ring.py +++ b/tests/test_ring.py @@ -27,6 +27,7 @@ def test_basic_attributes(self, mock): self.assertFalse(data.debug) self.assertEqual(1, len(data.chimes)) self.assertEqual(2, len(data.doorbells)) + self.assertEqual(1, len(data.stickup_cams)) self.assertFalse(data._persist_token) self.assertEquals('http://localhost/', data._push_token_notify_url) @@ -134,3 +135,47 @@ def test_doorbell_alerts(self, mock): self.assertIsInstance(dev.alert_expires_at, datetime) self.assertTrue(datetime.now() >= dev.alert_expires_at) self.assertIsNotNone(dev._ring.cache_file) + + @requests_mock.Mocker() + def test_stickup_cam_attributes(self, mock): + mock.get('https://api.ring.com/clients_api/ring_devices', + text=load_fixture('ring_devices.json')) + mock.get('https://api.ring.com/clients_api/doorbots/987652/health', + text=load_fixture('ring_doorboot_health_attrs.json')) + + data = self.ring_persistent + for dev in data.stickup_cams: + self.assertEqual('off', dev.lights) + self.assertEqual(0, dev.siren) + + @requests_mock.Mocker() + def test_stickup_cam_controls(self, mock): + mock.get('https://api.ring.com/clients_api/ring_devices', + text=load_fixture('ring_devices.json')) + mock.get('https://api.ring.com/clients_api/doorbots/987652/health', + text=load_fixture('ring_doorboot_health_attrs.json')) + mock.put(requests_mock.ANY, text='ok') + + data = self.ring_persistent + for dev in data.stickup_cams: + dev.lights = 'off' + dev.lights = 'on' + dev.siren = 0 + dev.siren = 30 + + history = list(filter(lambda x: x.method == 'PUT', + mock.request_history)) + self.assertEqual( + '/clients_api/doorbots/987652/floodlight_light_off', + history[0].path) + self.assertEqual( + '/clients_api/doorbots/987652/floodlight_light_on', + history[1].path) + self.assertEqual( + '/clients_api/doorbots/987652/siren_off', + history[2].path) + self.assertNotIn('duration', history[2].qs) + self.assertEqual( + '/clients_api/doorbots/987652/siren_on', + history[3].path) + self.assertEqual('30', history[3].qs['duration'][0])