-
-
Notifications
You must be signed in to change notification settings - Fork 38k
Update to DoorBirdPy v2 (again) #14933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
25a2432
eb3e3eb
696fa7c
46f2a3b
015d85f
e1d498f
b2a79c8
9a7f36b
ac4e58b
3719954
0f39fb1
2f11104
7649bd7
476a4a6
eb0448e
2ce67ea
45d93cc
f6d26f7
f4bb616
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.util import slugify | ||
|
|
||
| REQUIREMENTS = ['DoorBirdPy==0.1.3'] | ||
| REQUIREMENTS = ['doorbirdpy==2.0.4'] | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -24,21 +24,27 @@ | |
| API_URL = '/api/{}'.format(DOMAIN) | ||
|
|
||
| CONF_DOORBELL_EVENTS = 'doorbell_events' | ||
| CONF_MOTION_EVENTS = 'motion_events' | ||
| CONF_CUSTOM_URL = 'hass_url_override' | ||
| CONF_DOORBELL_NUMS = 'doorbell_numbers' | ||
|
|
||
| DOORBELL_EVENT = 'doorbell' | ||
| MOTION_EVENT = 'motionsensor' | ||
|
|
||
| # Sensor types: Name, device_class, event | ||
| SENSOR_TYPES = { | ||
| 'doorbell': ['Button', 'occupancy', DOORBELL_EVENT], | ||
| 'motion': ['Motion', 'motion', MOTION_EVENT], | ||
| 'doorbell': { | ||
| 'name': 'Button', | ||
| 'device_class': 'occupancy', | ||
| }, | ||
| 'motion': { | ||
| 'name': 'Motion', | ||
| 'device_class': 'motion', | ||
| }, | ||
| } | ||
|
|
||
| DEVICE_SCHEMA = vol.Schema({ | ||
| vol.Required(CONF_HOST): cv.string, | ||
| vol.Required(CONF_USERNAME): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( | ||
| cv.ensure_list, [cv.positive_int]), | ||
| vol.Optional(CONF_CUSTOM_URL): cv.string, | ||
| vol.Optional(CONF_NAME): cv.string, | ||
| vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): | ||
|
|
@@ -57,14 +63,18 @@ def setup(hass, config): | |
| from doorbirdpy import DoorBird | ||
|
|
||
| # Provide an endpoint for the doorstations to call to trigger events | ||
| hass.http.register_view(DoorbirdRequestView()) | ||
| hass.http.register_view(DoorBirdRequestView()) | ||
|
|
||
| # Provide an endpoint for the user to call to clear device changes | ||
| hass.http.register_view(DoorBirdCleanupView()) | ||
|
|
||
| doorstations = [] | ||
|
|
||
| for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): | ||
| device_ip = doorstation_config.get(CONF_HOST) | ||
| username = doorstation_config.get(CONF_USERNAME) | ||
| password = doorstation_config.get(CONF_PASSWORD) | ||
| doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) | ||
| custom_url = doorstation_config.get(CONF_CUSTOM_URL) | ||
| events = doorstation_config.get(CONF_MONITORED_CONDITIONS) | ||
| name = (doorstation_config.get(CONF_NAME) | ||
|
|
@@ -74,68 +84,39 @@ def setup(hass, config): | |
| status = device.ready() | ||
|
|
||
| if status[0]: | ||
| _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, | ||
| username) | ||
| doorstation = ConfiguredDoorbird(device, name, events, custom_url) | ||
| doorstation = ConfiguredDoorBird(device, name, events, custom_url, | ||
| doorbell_nums) | ||
| doorstations.append(doorstation) | ||
| _LOGGER.info('Connected to DoorBird "%s" as %s@%s', | ||
| doorstation.name, username, device_ip) | ||
| elif status[1] == 401: | ||
| _LOGGER.error("Authorization rejected by DoorBird at %s", | ||
| device_ip) | ||
| _LOGGER.error("Authorization rejected by DoorBird for %s@%s", | ||
| username, device_ip) | ||
| return False | ||
| else: | ||
| _LOGGER.error("Could not connect to DoorBird at %s: Error %s", | ||
| device_ip, str(status[1])) | ||
| _LOGGER.error("Could not connect to DoorBird as %s@%s: Error %s", | ||
| username, device_ip, str(status[1])) | ||
| return False | ||
|
|
||
| # SETUP EVENT SUBSCRIBERS | ||
| # Subscribe to doorbell or motion events | ||
| if events is not None: | ||
| # This will make HA the only service that receives events. | ||
| doorstation.device.reset_notifications() | ||
|
|
||
| # Subscribe to doorbell or motion events | ||
| subscribe_events(hass, doorstation) | ||
| doorstation.update_schedule(hass) | ||
|
|
||
| hass.data[DOMAIN] = doorstations | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def subscribe_events(hass, doorstation): | ||
| """Initialize the subscriber.""" | ||
| for sensor_type in doorstation.monitored_events: | ||
| name = '{} {}'.format(doorstation.name, | ||
| SENSOR_TYPES[sensor_type][0]) | ||
| event_type = SENSOR_TYPES[sensor_type][2] | ||
|
|
||
| # Get the URL of this server | ||
| hass_url = hass.config.api.base_url | ||
|
|
||
| # Override url if another is specified onth configuration | ||
| if doorstation.custom_url is not None: | ||
| hass_url = doorstation.custom_url | ||
|
|
||
| slug = slugify(name) | ||
|
|
||
| url = '{}{}/{}'.format(hass_url, API_URL, slug) | ||
|
|
||
| _LOGGER.info("DoorBird will connect to this instance via %s", | ||
| url) | ||
|
|
||
| _LOGGER.info("You may use the following event name for automations" | ||
| ": %s_%s", DOMAIN, slug) | ||
|
|
||
| doorstation.device.subscribe_notification(event_type, url) | ||
|
|
||
|
|
||
| class ConfiguredDoorbird(): | ||
| class ConfiguredDoorBird(object): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expected 2 blank lines, found 1 |
||
| """Attach additional information to pass along with configured device.""" | ||
|
|
||
| def __init__(self, device, name, events=None, custom_url=None): | ||
| def __init__(self, device, name, events, custom_url, doorbell_nums): | ||
| """Initialize configured device.""" | ||
| self._name = name | ||
| self._device = device | ||
| self._custom_url = custom_url | ||
| self._monitored_events = events | ||
| self._doorbell_nums = doorbell_nums | ||
|
|
||
| @property | ||
| def name(self): | ||
|
|
@@ -152,16 +133,137 @@ def custom_url(self): | |
| """Custom url for device.""" | ||
| return self._custom_url | ||
|
|
||
| @property | ||
| def monitored_events(self): | ||
| """Get monitored events.""" | ||
| if self._monitored_events is None: | ||
| return [] | ||
| def update_schedule(self, hass): | ||
| """Register monitored sensors and deregister others.""" | ||
| from doorbirdpy import DoorBirdScheduleEntrySchedule | ||
|
|
||
| # Create a new schedule (24/7) | ||
| schedule = DoorBirdScheduleEntrySchedule() | ||
| schedule.add_weekday(0, 604800) # seconds in a week | ||
|
|
||
| # Get the URL of this server | ||
| hass_url = hass.config.api.base_url | ||
|
|
||
| # Override url if another is specified in the configuration | ||
| if self.custom_url is not None: | ||
| hass_url = self.custom_url | ||
|
|
||
| # For all sensor types (enabled + disabled) | ||
| for sensor_type in SENSOR_TYPES: | ||
| name = '{} {}'.format(self.name, SENSOR_TYPES[sensor_type]['name']) | ||
| slug = slugify(name) | ||
| url = '{}{}/{}'.format(hass_url, API_URL, slug) | ||
|
|
||
| if sensor_type in self._monitored_events: | ||
| # Enabled -> register | ||
| self._register_event(url, sensor_type, schedule) | ||
| _LOGGER.info('Registered for %s pushes from DoorBird "%s". ' | ||
| 'Use the "%s_%s" event for automations.', | ||
| sensor_type, self.name, DOMAIN, slug) | ||
| else: | ||
| # Disabled -> deregister | ||
| self._deregister_event(url, sensor_type) | ||
| _LOGGER.info('Deregistered %s pushes from DoorBird "%s". ' | ||
| 'If any old favorites or schedules remain, ' | ||
| 'follow the instructions in the component ' | ||
| 'documentation to clear device registrations.', | ||
| sensor_type, self.name) | ||
|
|
||
| def _register_event(self, hass_url, event, schedule): | ||
| """Add a schedule entry in the device for a sensor.""" | ||
| from doorbirdpy import DoorBirdScheduleEntryOutput | ||
|
|
||
| # Register HA URL as webhook if not already, then get the ID | ||
| if not self.webhook_is_registered(hass_url): | ||
| self.device.change_favorite('http', | ||
| 'Home Assistant on {} ({} events)' | ||
| .format(hass_url, event), hass_url) | ||
| fav_id = self.get_webhook_id(hass_url) | ||
|
|
||
| if not fav_id: | ||
| _LOGGER.warning('Could not find favorite for URL "%s". ' | ||
| 'Skipping sensor "%s".', hass_url, event) | ||
| return | ||
|
|
||
| # Add event handling to device schedule | ||
| output = DoorBirdScheduleEntryOutput(event='http', | ||
| param=fav_id, | ||
| schedule=schedule) | ||
|
|
||
| if event == 'doorbell': | ||
| # Repeat edit for each monitored doorbell number | ||
| for doorbell in self._doorbell_nums: | ||
| entry = self.device.get_schedule_entry(event, str(doorbell)) | ||
| entry.output.append(output) | ||
| self.device.change_schedule(entry) | ||
| else: | ||
| entry = self.device.get_schedule_entry(event) | ||
| entry.output.append(output) | ||
| self.device.change_schedule(entry) | ||
|
|
||
| def _deregister_event(self, hass_url, event): | ||
| """Remove the schedule entry in the device for a sensor.""" | ||
| # Find the right favorite and delete it | ||
| fav_id = self.get_webhook_id(hass_url) | ||
| if not fav_id: | ||
| return | ||
|
|
||
| self._device.delete_favorite('http', fav_id) | ||
|
|
||
| if event == 'doorbell': | ||
| # Delete the matching schedule for each doorbell number | ||
| for doorbell in self._doorbell_nums: | ||
| self._delete_schedule_action(event, fav_id, str(doorbell)) | ||
| else: | ||
| self._delete_schedule_action(event, fav_id) | ||
|
|
||
| def _delete_schedule_action(self, sensor, fav_id, param=""): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why should this method be defined in home assistant? Looks like it belongs in the interface library.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what HA is doing here is kind of an edge case and it would not be useful in most other applications of the library.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't registering a webhook a common use case?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doorbird API requires that webhooks be registered on the device. The registered webhooks are called when an event happens, but are also limited to when they can be fired by a user defined schedule. The nomenclature could be clearer, but clearing a schedule is simply removing a webhook. Doorbird API does not scope access to created schedules/webhooks, so each app is responsible for playing nice with other entries that may have been created by another app. The code here is simply making sure that the appropriate schedule/webhook is being removed without blowing away all of the entries. |
||
| """ | ||
| Remove the HA output from a schedule. | ||
| """ | ||
| entries = self._device.schedule() | ||
| for entry in entries: | ||
| if entry.input != sensor or entry.param != param: | ||
| continue | ||
|
|
||
| for action in entry.output: | ||
| if action.event == 'http' and action.param == fav_id: | ||
| entry.output.remove(action) | ||
|
|
||
| return self._monitored_events | ||
| self._device.change_schedule(entry) | ||
|
|
||
| def webhook_is_registered(self, ha_url, favs=None) -> bool: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DoorBirdPy is designed to be simple and does not offer caching, nor do I have access to a physical DoorBird anymore to test such a feature (I'm moving). The way this method is implemented in HA makes use of caching by accessing data that was retrieved during HA startup. |
||
| """Return whether the given URL is registered as a device favorite.""" | ||
| favs = favs if favs else self.device.favorites() | ||
|
|
||
| class DoorbirdRequestView(HomeAssistantView): | ||
| if 'http' not in favs: | ||
| return False | ||
|
|
||
| for fav in favs['http'].values(): | ||
| if fav['value'] == ha_url: | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
| def get_webhook_id(self, ha_url, favs=None) -> str or None: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same explanation as |
||
| """ | ||
| Return the device favorite ID for the given URL. | ||
|
|
||
| The favorite must exist or there will be problems. | ||
| """ | ||
| favs = favs if favs else self.device.favorites() | ||
|
|
||
| if 'http' not in favs: | ||
| return None | ||
|
|
||
| for fav_id in favs['http']: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to delete all favorites via the Doorbird app which will cause a KeyError because 'http' key is missing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an old version of the code, this function is now called
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have changes that aren't pushed? Ran into that while testing the schedule clearing.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought I had pushed them (just renaming some things), but apparent I made them against the branch that didn't have your changes on it 🤦♂️ |
||
| if favs['http'][fav_id]['value'] == ha_url: | ||
| return fav_id | ||
|
|
||
| return None | ||
|
|
||
|
|
||
| class DoorBirdRequestView(HomeAssistantView): | ||
| """Provide a page for the device to call.""" | ||
|
|
||
| requires_auth = False | ||
|
|
@@ -173,8 +275,49 @@ class DoorbirdRequestView(HomeAssistantView): | |
| @asyncio.coroutine | ||
| def get(self, request, sensor): | ||
| """Respond to requests from the device.""" | ||
| from aiohttp import web | ||
| hass = request.app['hass'] | ||
|
|
||
| hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor)) | ||
|
|
||
| return 'OK' | ||
| return web.Response(status=200, text='OK') | ||
|
|
||
|
|
||
| class DoorBirdCleanupView(HomeAssistantView): | ||
| """Provide a URL to call to delete ALL webhooks/schedules.""" | ||
|
|
||
| requires_auth = False | ||
| url = API_URL + '/clear/{name}' | ||
| name = 'DoorBird Cleanup' | ||
|
|
||
| # pylint: disable=no-self-use | ||
| async def get(self, request, name): | ||
| """Act on requests.""" | ||
| from aiohttp import web | ||
| hass = request.app['hass'] | ||
|
|
||
| # Find matching device | ||
| for config_device in hass.data[DOMAIN]: | ||
| if config_device.name == name: | ||
| return self._clear(config_device.device) | ||
|
|
||
| # No matching device | ||
| return web.Response(status=404, | ||
| text='Device "{}" not found'.format(name)) | ||
|
|
||
| @staticmethod | ||
| def _clear(device): | ||
| from aiohttp import web | ||
|
|
||
| # Clear webhooks | ||
| favorites = device.favorites() | ||
| for favorite_type in favorites: | ||
| for favorite_id in favorites[favorite_type]: | ||
| device.delete_favorite(favorite_type, favorite_id) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is does doing I/O? You're calling this from an async method which does not allow doing I/O |
||
|
|
||
| # Clear schedule | ||
| schedule = device.schedule() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is does doing I/O? You're calling this from an async method which does not allow doing I/O
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need to do I/O, do |
||
| for entry in schedule: | ||
| device.delete_schedule(entry.input, entry.param) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blank line at end of file |
||
| return web.Response(status=202) | ||
Uh oh!
There was an error while loading. Please reload this page.