Skip to content

Add PostNL sensor (Dutch Postal Services)#12366

Merged
balloob merged 24 commits intohome-assistant:devfrom
iMicknl:dev
May 2, 2018
Merged

Add PostNL sensor (Dutch Postal Services)#12366
balloob merged 24 commits intohome-assistant:devfrom
iMicknl:dev

Conversation

@iMicknl
Copy link
Copy Markdown
Member

@iMicknl iMicknl commented Feb 13, 2018

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):

- sensors
  - platform: postnl
    name: Mick
    username: [jouw.postnl.nl email]
    password: [jouw.postnl.nl password]

Checklist:

  • The code change is tested and works locally.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

# 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.
@iMicknl iMicknl requested a review from andrey-git as a code owner February 13, 2018 06:59
@homeassistant
Copy link
Copy Markdown
Contributor

Hi @iMicknl,

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (115 > 79 characters)

_LOGGER.exception('Wrong Credentials')
return False

add_devices([PostNLSensor(username, password, name, update_interval)], True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (80 > 79 characters)

from postnl_api import PostNL_API

try:
api = PostNL_API(username, password)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local variable 'api' is assigned to but never used

Copy link
Copy Markdown
Contributor

@thijsdejong thijsdejong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

@OttoWinter OttoWinter Feb 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

@OttoWinter OttoWinter Feb 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a suggestion, but this seems like more of a thing the pip package should do instead of Home Assistant's code base...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'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)):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify the exact exceptions that you expect.


except Exception:
_LOGGER.exception("Can't connect to the PostNL webservice")
return False
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include these parsing instructions in the postnl_api package and just return a parsed data structure to Home Assistant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (93 > 79 characters)


def __init__(self, api, name, interval):
"""Initialize the sensor."""
self.friendly_name = name
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.


self._api = api

self.update = Throttle(interval)(self._update)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me a little out with this one? put the throttle as a decorator on the update method.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'config'
line too long (98 > 79 characters)

@iMicknl
Copy link
Copy Markdown
Member Author

iMicknl commented Apr 22, 2018

@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? :)

@iMicknl iMicknl changed the title WIP: Add PostNL sensor (Dutch Postal Services) Add PostNL sensor (Dutch Postal Services) Apr 28, 2018
Comment thread homeassistant/components/sensor/postnl.py Outdated

ICON = 'mdi:package-variant-closed'

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be less aggressive polling for new packages. Let's change this to 30 minutes.

Copy link
Copy Markdown
Member Author

@iMicknl iMicknl May 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

USPS (American equivalent) has been IP banning people using the HASS integration…

@balloob
Copy link
Copy Markdown
Member

balloob commented May 2, 2018

Ok to merge when you run script/gen_requirements_all.py and commit the changes.

@balloob balloob merged commit b66be59 into home-assistant:dev May 2, 2018
@balloob
Copy link
Copy Markdown
Member

balloob commented May 2, 2018

Congrats on getting your first PR merged 🎉

@keesschollaart81
Copy link
Copy Markdown
Contributor

Nice job @iMicknl

@iMicknl
Copy link
Copy Markdown
Member Author

iMicknl commented May 2, 2018

@balloob haha, it takes some time but finally. Thanks to all the reviewers! 👍 This was good for my Python skills.

@balloob balloob mentioned this pull request May 11, 2018
@home-assistant home-assistant locked and limited conversation to collaborators Sep 5, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants