Add PostNL sensor (Dutch Postal Services)#12366
Add PostNL sensor (Dutch Postal Services)#12366balloob merged 24 commits intohome-assistant:devfrom iMicknl:dev
Conversation
# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
| return datetime.strptime(date.group(1).replace(' ', '')[:-6], '%Y-%m-%dT%H:%M:%S').strftime('%d-%m-%Y') | ||
|
|
||
| def parse_time(date): | ||
| return datetime.strptime(date.group(1).replace(' ', '')[:-6], '%Y-%m-%dT%H:%M:%S').strftime('%H:%M') |
There was a problem hiding this comment.
line too long (112 > 79 characters)
| status_counts = defaultdict(str) | ||
|
|
||
| def parse_date(date): | ||
| return datetime.strptime(date.group(1).replace(' ', '')[:-6], '%Y-%m-%dT%H:%M:%S').strftime('%d-%m-%Y') |
There was a problem hiding this comment.
line too long (115 > 79 characters)
| _LOGGER.exception('Wrong Credentials') | ||
| return False | ||
|
|
||
| add_devices([PostNLSensor(username, password, name, update_interval)], True) |
There was a problem hiding this comment.
line too long (80 > 79 characters)
| from postnl_api import PostNL_API | ||
|
|
||
| try: | ||
| api = PostNL_API(username, password) |
There was a problem hiding this comment.
local variable 'api' is assigned to but never used
thijsdejong
left a comment
There was a problem hiding this comment.
Not a code-reviewer or anything, just pointing out things I noticed that might need some improvement
| api = PostNL_API(username, password) | ||
|
|
||
| except Exception: | ||
| _LOGGER.exception('Wrong Credentials') |
There was a problem hiding this comment.
Might be better if this is changed to 'Error when logging in'. It currently will say Wrong Credentials if the PostNL site is down.
| @property | ||
| def name(self): | ||
| """Return the name of the sensor.""" | ||
| return self._name or DOMAIN |
There was a problem hiding this comment.
Instead, consider using vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, and DEFAULT_NAME = 'postnl' - it's cleaner to make voluptuous do default value handling.
| if self._state == 1: | ||
| return 'package' | ||
| else: | ||
| return 'packages' |
There was a problem hiding this comment.
pylint complains about a if-else-return here. Write this instead:
if self._state == 1:
return 'package'
return 'packages'|
|
||
| api = PostNL_API(self._username, self._password) | ||
| shipments = api.get_relevant_shipments() | ||
| status_counts = defaultdict(str) |
There was a problem hiding this comment.
Why is this a defaultdict? I mean as far as I can see here, a normal built-in dictionary would work just fine...
| self._attributes = { | ||
| ATTR_ATTRIBUTION: 'Information provided by PostNL' | ||
| } | ||
| self._attributes.update(status_counts) |
There was a problem hiding this comment.
You could just write
self._attributes = {
ATTR_ATTRIBUTION: 'Information provided by PostNL',
**status_counts
}or alternatively, just remove status_counts and set the _attributes directly (then of course also removing one from self._state = len(status_counts)). Next, the attribution constant string "Information provided by PostNL" could also potentially be moved to a global constant (WUnderground, for example, does this - keeps the code a bit cleaner)
|
|
||
| for shipment in shipments: | ||
| status = shipment['status']['formatted']['short'] | ||
| status = re.sub(r'{(?:Date|dateAbs):(.*?)}', parse_date, status) |
There was a problem hiding this comment.
Just a suggestion, but this seems like more of a thing the pip package should do instead of Home Assistant's code base...
There was a problem hiding this comment.
I was in doubt about this one. The pip package is just a general wrapper for the PostNL API and I think this is something in the presentation layer, which shouldn't be in the pip package. However; maybe I should add some helper functions in the pip package in order to parse date / times.
| """Update device state.""" | ||
| from postnl_api import PostNL_API | ||
|
|
||
| api = PostNL_API(self._username, self._password) |
There was a problem hiding this comment.
The setup of the API is already done in setup_platform. Pass that to PostNLSensor().
|
|
||
| def parse_time(date): | ||
| return datetime.strptime(date.group(1) | ||
| .replace(' ', '')[:-6], '%Y-%m-%dT%H:%M:%S').strftime('%H:%M') |
There was a problem hiding this comment.
line too long (99 > 79 characters)
|
|
||
| def parse_date(date): | ||
| return datetime.strptime(date.group(1) | ||
| .replace(' ', '')[:-6], '%Y-%m-%dT%H:%M:%S').strftime('%d-%m-%Y') |
There was a problem hiding this comment.
line too long (102 > 79 characters)
| For more details about this platform, please refer to the documentation at | ||
| https://home-assistant.io/components/sensor.postnl/ | ||
| """ | ||
| from collections import defaultdict |
There was a problem hiding this comment.
'collections.defaultdict' imported but unused
| vol.Required(CONF_USERNAME): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
| vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=1800)): |
There was a problem hiding this comment.
No need to implement this.
Implement the default as a constant SCAN_INTERVAL. Home Assistant will be able to pick that up and users can override it via their config: https://home-assistant.io/docs/configuration/platform_options/#scan-interval
| try: | ||
| api = PostNL_API(username, password) | ||
|
|
||
| except Exception: |
There was a problem hiding this comment.
Specify the exact exceptions that you expect.
|
|
||
| except Exception: | ||
| _LOGGER.exception("Can't connect to the PostNL webservice") | ||
| return False |
There was a problem hiding this comment.
Platforms don't return anything. Just return
| def unit_of_measurement(self): | ||
| """Return the unit of measurement of this entity, if any.""" | ||
|
|
||
| if self._state == 1: |
There was a problem hiding this comment.
unit should be static. Not being static breaks the history grouping. It sees it as different units. Just stick with packages.
| shipments = self._api.get_relevant_shipments() | ||
| status_counts = {} | ||
|
|
||
| def parse_date(date): |
There was a problem hiding this comment.
homeassistant.util.dt has parsers for datetime, date and time in RFC3339 format.
|
|
||
| for shipment in shipments: | ||
| status = shipment['status']['formatted']['short'] | ||
| status = re.sub(r'{(?:Date|dateAbs):(.*?)}', parse_date, status) |
There was a problem hiding this comment.
Please include these parsing instructions in the postnl_api package and just return a parsed data structure to Home Assistant.
There was a problem hiding this comment.
I will add is as a separate helper function in postnl_api, however I won't enable it by default since it tampers with the default output and it is also a small performance hit. However, I agree that this kind of parsing logic shouldn't be in the Home Assistant plugin itself.
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components.sensor import PLATFORM_SCHEMA | ||
| from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, |
There was a problem hiding this comment.
line too long (93 > 79 characters)
|
|
||
| def __init__(self, api, name, interval): | ||
| """Initialize the sensor.""" | ||
| self.friendly_name = name |
There was a problem hiding this comment.
Remove this. This is for users to set.
| vol.Required(CONF_USERNAME): cv.string, | ||
| vol.Required(CONF_PASSWORD): cv.string, | ||
| vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
| vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): |
There was a problem hiding this comment.
Remove this. It's already part of the config schema.
| username = config.get(CONF_USERNAME) | ||
| password = config.get(CONF_PASSWORD) | ||
| name = config.get(CONF_NAME) | ||
| update_interval = config.get(CONF_SCAN_INTERVAL) |
|
|
||
| self._api = api | ||
|
|
||
| self.update = Throttle(interval)(self._update) |
There was a problem hiding this comment.
You can use the throttle if you want to limit the update interval customization. Create a constant for this and use that together with the throttle, eg MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60). The throttle will only be active per entity though so multiple entities can still hit the api faster than the throttle allows.
Please rename _update to update and put the throttle as a decorator on the update method.
There was a problem hiding this comment.
Can you help me a little out with this one? put the throttle as a decorator on the update method.
There was a problem hiding this comment.
Also, you said that SCAN_INTERVAL is already part of the config scheme. Why not use this for the throttling? So refer to SCAN_INTERVAL instead of creating a custom constant called MIN_TIME_BETWEEN_UPDATES.
There was a problem hiding this comment.
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
...There was a problem hiding this comment.
You don't want the throttling time to be configurable. You should decide on the min time between updates. Users will be able to customize the update interval down to that time.
There was a problem hiding this comment.
Ok, thanks. Updated!
However, I am a bit lost how to implement the SCAN_INTERVAL and a default. The API can be queried every minute if you would like to, however I would suggest a default of every 15 minutes (or more) since this doesn't change that often. Any examples of other components who do something like this?
There was a problem hiding this comment.
Just define SCAN_INTERVAL as a constant with the appropriate time interval at the module level. You can search for SCAN_INTERVAL to find examples.
| import logging | ||
| from datetime import timedelta, datetime | ||
| import re | ||
| import voluptuous as vol |
There was a problem hiding this comment.
Add a blank line between standard library and 3rd party imports.
| self._state = None | ||
|
|
||
| self._api = api | ||
| self.update = Throttle(config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))(self.update) |
There was a problem hiding this comment.
undefined name 'config'
line too long (98 > 79 characters)
|
@balloob / @MartinHjelmare thanks for the extensive reviews, both of you! I think I applied all the feedback and made the adjustments also in the Python wrapper + documentation. Is it possible to review it for a last time, so I can finish it and ship it finally? :) |
|
|
||
| ICON = 'mdi:package-variant-closed' | ||
|
|
||
| MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) |
There was a problem hiding this comment.
We should be less aggressive polling for new packages. Let's change this to 30 minutes.
There was a problem hiding this comment.
True. In the beginning this was different, where the default was around 30. But where the user could change it himself. This changed after feedback..
Changed it to 30 minutes again.
There was a problem hiding this comment.
USPS (American equivalent) has been IP banning people using the HASS integration…
# Conflicts: # homeassistant/components/sensor/postnl.py
|
Ok to merge when you run |
|
Congrats on getting your first PR merged 🎉 |
|
Nice job @iMicknl |
|
@balloob haha, it takes some time but finally. Thanks to all the reviewers! 👍 This was good for my Python skills. |
Description:
I have been working on a plugin which adds a PostNL sensor, which can be used for tracking package deliveries. It can be used to track multiple accounts, which is especially handy for households with several people.
Is there someone who can help me out with a code review / show me which parts I have to improve? I haven't touched Python before, so I think there are some parts I need to improve before merging. 👍
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io/pull/5231*
Example entry for
configuration.yaml(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
toxrun successfully. Your PR cannot be merged unless tests passREQUIREMENTSvariable (example).requirements_all.txtby runningscript/gen_requirements_all.py..coveragerc.