Skip to content

Commit 6cbeefe

Browse files
Initial public release.
Moving this out of my personal configuration and publishing for HACS.
1 parent 1ca2aec commit 6cbeefe

File tree

5 files changed

+206
-2
lines changed

5 files changed

+206
-2
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,29 @@
1-
# package-concierge-hass
2-
Home Assistant Custom Component for Package Concierge
1+
# Package Concierge Scraper Component for Home Assistant
2+
3+
Home Assistant custom component for [Package Concierge](https://packageconciergeadmin.com) that can login and get a count (maximum 5) of packages waiting to be picked up from the Package Concierge package locker system.
4+
It also supports multiple users by registering multiple sensors.
5+
6+
It exposes a sensor (by default `sensor.package_concierge_USERNAME`, but overridden by setting `name:`) that is a count of the number of packages in the "Delivered" status.
7+
8+
## Installation Using HACS
9+
10+
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)
11+
12+
This custom component can be installed using HACS. Follow [these steps](https://hacs.xyz/docs/faq/custom_repositories) and use the repository URL `https://github.com/corbanmailloux/package-concierge-hass`
13+
14+
## Manual Installation
15+
16+
Copy the contents of the `custom_components/package_concierge/` folder to `<config_dir>/custom_components/package_concierge/` on your Home Assistant installation.
17+
18+
## Configuration
19+
20+
Add the following to your `configuration.yaml` file (or a package file):
21+
22+
```yaml
23+
# Example configuration.yaml entry
24+
sensor:
25+
- platform: package_concierge
26+
name: package_concierge_corban # Optional
27+
username: "USERNAME"
28+
password: "0000" # PIN
29+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Integration for Package Concierge."""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"domain": "package_concierge",
3+
"name": "Package Concierge",
4+
"documentation": "https://github.com/corbanmailloux/package-concierge-hass",
5+
"issue_tracker": "https://github.com/corbanmailloux/package-concierge-hass/issues",
6+
"requirements": [
7+
"requests",
8+
"beautifulsoup4"
9+
],
10+
"dependencies": [],
11+
"codeowners": [
12+
"@corbanmailloux"
13+
],
14+
"iot_class": "cloud_polling",
15+
"version": "1.1.0"
16+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Platform for sensor integration of Package Concierge."""
2+
3+
import logging
4+
5+
import voluptuous as vol
6+
import homeassistant.helpers.config_validation as cv
7+
from homeassistant.components.sensor import PLATFORM_SCHEMA
8+
9+
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_NAME
10+
from homeassistant.helpers.entity import Entity
11+
from datetime import timedelta
12+
13+
_LOGGER = logging.getLogger(__name__)
14+
15+
# Validation of the user's configuration
16+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
17+
{
18+
vol.Optional(CONF_NAME): cv.string,
19+
vol.Required(CONF_USERNAME): cv.string,
20+
vol.Required(CONF_PASSWORD): cv.string,
21+
}
22+
)
23+
24+
SCAN_INTERVAL = timedelta(minutes=60)
25+
26+
27+
def setup_platform(hass, config, add_entities, discovery_info=None):
28+
"""Set up the sensor platform."""
29+
30+
name = config.get(CONF_NAME)
31+
username = config.get(CONF_USERNAME)
32+
password = config.get(CONF_PASSWORD)
33+
34+
if name is None:
35+
name = f"package_concierge_{username}"
36+
37+
scraper = PackageConciergeScraper(username, password)
38+
39+
add_entities([PackageConciergeSensor(scraper, name)])
40+
41+
42+
class PackageConciergeSensor(Entity):
43+
"""Representation of a Sensor."""
44+
45+
def __init__(self, scraper, name):
46+
"""Initialize the sensor."""
47+
self._state = None
48+
self._scraper = scraper
49+
self._name = name
50+
51+
@property
52+
def name(self):
53+
"""Return the name of the sensor."""
54+
return self._name
55+
56+
@property
57+
def icon(self):
58+
return "mdi:locker-multiple"
59+
60+
@property
61+
def state(self):
62+
"""Return the state of the sensor."""
63+
return self._state
64+
65+
@property
66+
def unit_of_measurement(self):
67+
"""Return the unit of measurement."""
68+
return "package(s)"
69+
70+
def update(self):
71+
"""Fetch new state data for the sensor.
72+
73+
This is the only method that should fetch new data for Home Assistant.
74+
"""
75+
self._scraper.update()
76+
self._state = self._scraper.number_of_packages
77+
78+
79+
# The stuff below should be extracted into a Python package, but that's a future problem.
80+
import requests
81+
from bs4 import BeautifulSoup
82+
83+
84+
class PackageConciergeScraper:
85+
def __init__(self, username, password):
86+
"""Create a new scraper with the given settings."""
87+
self._username = username
88+
self._password = password
89+
self._number_of_packages = None
90+
91+
@property
92+
def number_of_packages(self):
93+
return self._number_of_packages
94+
95+
def update(self):
96+
_LOGGER.debug(f"Package Concierge running update for: {self._username}")
97+
98+
login_url = "https://packageconciergeadmin.com/Login.aspx"
99+
100+
# Create a persistent session for cookies.
101+
session = requests.Session()
102+
103+
login_response = session.get(login_url)
104+
login_page = BeautifulSoup(login_response.content, features="html.parser")
105+
106+
# Extract the required fields:
107+
view_state = login_page.find("input", {"name": "__VIEWSTATE"})["value"]
108+
view_state_generator = login_page.find(
109+
"input", {"name": "__VIEWSTATEGENERATOR"}
110+
)["value"]
111+
event_validation = login_page.find("input", {"name": "__EVENTVALIDATION"})[
112+
"value"
113+
]
114+
115+
_LOGGER.debug(
116+
f"During update for: {self._username}, view_state: {view_state}, view_state_generator: {view_state_generator}, event_validation: {event_validation}."
117+
)
118+
119+
form_data = {
120+
"__VIEWSTATE": view_state,
121+
"__VIEWSTATEGENERATOR": view_state_generator,
122+
"__EVENTVALIDATION": event_validation,
123+
"ctl00$ctl00$MainContent$MainContent$txt_Username": self._username,
124+
"ctl00$ctl00$MainContent$MainContent$txt_Password": self._password,
125+
"ctl00$ctl00$MainContent$MainContent$btn_Submit.x": 42, # These values can be anything, but they must be set.
126+
"ctl00$ctl00$MainContent$MainContent$btn_Submit.y": 21,
127+
}
128+
response = session.post(login_url, data=form_data)
129+
130+
if response.url != "https://packageconciergeadmin.com/Welcome.aspx":
131+
_LOGGER.error(
132+
f"Package Concierge login failed for username: {self._username}"
133+
)
134+
135+
raise RuntimeError("Login failed.")
136+
137+
# Login worked. Use the session to get the member page.
138+
response = session.get("https://packageconciergeadmin.com/Member/Summary.aspx")
139+
summary_page = BeautifulSoup(response.content, features="html.parser")
140+
141+
table = summary_page.find(
142+
"table",
143+
{
144+
"id": "ctl00_ctl00_ctl00_MainContent_MainContent_MainContent_grid_PackageActivityGrid_ctl00"
145+
},
146+
)
147+
rows = table.find_all("tbody")[-1].find_all("tr")
148+
_LOGGER.debug(f"During update for: {self._username}, all rows: {rows}")
149+
150+
number_of_packages = 0
151+
for row in rows:
152+
if row.find_all("td")[2].text == "Delivered":
153+
number_of_packages += 1
154+
155+
self._number_of_packages = number_of_packages

hacs.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Package Concierge Scraper",
3+
"render_readme": true,
4+
"country": "US"
5+
}

0 commit comments

Comments
 (0)