2dehands Home Assistant custom component. It provides sensors with info related to your 2dehands account and profile.
It can automatically extend active items that are close to automatic removal when the 2dehands website exposes a callable extension action.
- HACS: add this repo as a custom repot in HACS and search for the integration in the list of HACS
- Restart Home Assistant
- Add '2dehands.be' integration via HA Settings > 'Devices and Services' > 'Integrations'
- Provide 2dehands.be username and password
After setup, the integration creates sensors and buttons for your configured 2dehands profile:
| Sensor | Description |
|---|---|
Items being sold |
Number of active items found in the account. |
Favorite items |
Number of favorite items found on the account favorites page. |
Unread messages |
Number of unread account messages. |
Unread notifications |
Number of unread account notifications. |
Extendable items |
Number of active items where 2dehands exposes the extension link. |
Items close to removal |
Number of active items within the configured extension threshold. |
Favorite ads with a recognizable location also create geolocation entities. Their coordinates use location data supplied by 2dehands when available, otherwise the city is matched locally against the bundled GeoNames dataset. The coordinate is therefore the approximate city center, not the seller's address. Favorite city names are not sent to an external geocoding service.
The Items being sold sensor includes attributes for the extension settings, the last extension result, and a sold_items list with item titles, prices, statuses, image URLs, view counts, extension flags, and URLs fetched from https://www.2dehands.be/my-account/sell/api/listings.
The Favorite items sensor includes a favorite_items attribute with favorite item names, prices, statuses, image URLs, locations, and URLs fetched from https://www.2dehands.be/my-account/favorites/favorites.json. The Items close to removal sensor includes an expiring_items attribute with the active items currently inside the extension window. Extension timing uses showExtensionLink, expiring, extensionPending, and closeDate; durationInDays is not used because it describes the listing duration, not the remaining time. When extension is attempted, the integration only calls extension actions discovered from the listing payload or sell overview.
Unread message and notification counts are fetched from https://www.2dehands.be/header/messages/message-count and https://www.2dehands.be/header/notifications/notification-count.
The integration creates a Refresh website data button entity for an immediate manual refresh. Automatic website refreshes remain scheduled every hour. For every own active ad, the integration also creates a Duplicate ... button entity. Pressing this button calls the duplicate service for that source ad and refreshes the integration data afterwards.
Two-factor authentication is detected during setup, but cannot be completed inside Home Assistant.
To show all favorite ads on one dashboard map, edit a dashboard, add a Manual card, and paste the following YAML:
type: map
title: Favorite 2dehands ads
aspect_ratio: 16:9
auto_fit: true
hours_to_show: 0
cluster: true
geo_location_sources:
- 2dehandsThe 2dehands geolocation source automatically selects every favorite ad that
has a recognized location, so individual entity IDs do not need to be added to
the card. New favorites appear automatically and removed favorites disappear
after the next integration refresh. Each marker uses the ad's main picture and
its entity state contains the advertised price. Markers near each other may be
grouped into a cluster; select the cluster to reveal its ads. Ads without a
location that can be matched are omitted. Selecting a marker exposes a
clickable, absolute url attribute that opens the advertisement on 2dehands.
Favorites whose status is Removed are also omitted from the map.
Approximate fallback coordinates are derived from a bundled Belgian/Dutch subset of GeoNames data, licensed under CC BY 4.0.
Following services 2dehands will be available:
-
2dehands.extend_sales: extend active items which can be extendedservice: 2dehands.extend_sales data: threshold_days: 3 force: false
Optional fields:
Field Description config_entry_idLimit the service call to one configured account. When omitted, all configured accounts are processed. threshold_daysExtend items when automatic removal is within this many days. Defaults to the integration option. forceExtend every active item where 2dehands exposes the extension link, even if it is not close to removal yet. -
2dehands.duplicate_ad: duplicate one of your own adsservice: 2dehands.duplicate_ad data: item_id: m2251423896
Optional fields:
Field Description config_entry_idRequired when multiple 2dehands accounts are configured. item_idItem ID of the own ad to copy. The service reads the original ad from
/seller/view/<item_id>, starts the placement form for the same category and title, uploads copies of the original images, then posts a new free ad to/plaats/ads.
Still some optimisations are planned, see Issues section in GitHub.
The main logic and API connection related code can be found within source code 2dehands/custom_components/2dehands:
- api.py -> browser API client, authentication, item normalization and extension calls
- button.py -> manual refresh and duplicate buttons
- coordinator.py -> data refresh and automatic extension logic
- sensor.py
- __init__.py -> setup, unload and services
- config_flow.py -> profile authentication and options flow
All other files just contain boilerplate code for the integration to work within HA or to have some constants/strings/translations.
alias: Extend 2dehands items every morning
trigger:
- platform: time
at: "08:00:00"
action:
- service: 2dehands.extend_sales
data:
threshold_days: 3The template searches for sensors whose names start with 2dehands, so it also works when Home Assistant adds your account name to the generated entity IDs.
type: markdown
title: 2dehands
content: >
{% set own_ads_sensors = states.sensor | selectattr('name', 'search',
'(?i)^2dehands.*items being sold$') | map(attribute='entity_id') | list %}
{% set favorite_ads_sensors = states.sensor | selectattr('name', 'search',
'(?i)^2dehands.*favorite items$') | map(attribute='entity_id') | list %}
{% set unread_messages_sensors = states.sensor | selectattr('name', 'search',
'(?i)^2dehands.*unread messages$') | map(attribute='entity_id') | list %}
{% set unread_notifications_sensors = states.sensor | selectattr('name',
'search', '(?i)^2dehands.*unread notifications$') | map(attribute='entity_id')
| list %}
{% set ns = namespace(own_ads=[], favorite_ads=[], unread_messages=0,
unread_notifications=0) %}
{% for sensor in own_ads_sensors %}
{% set ns.own_ads = ns.own_ads + (state_attr(sensor, 'sold_items') or []) %}
{% endfor %}
{% for sensor in favorite_ads_sensors %}
{% set ns.favorite_ads = ns.favorite_ads + (state_attr(sensor, 'favorite_items') or []) %}
{% endfor %}
{% for sensor in unread_messages_sensors %}
{% set ns.unread_messages = ns.unread_messages + (states(sensor) | int(0)) %}
{% endfor %}
{% for sensor in unread_notifications_sensors %}
{% set ns.unread_notifications = ns.unread_notifications + (states(sensor) | int(0)) %}
{% endfor %}
{% set own_ads = ns.own_ads %}
{% set favorite_ads = ns.favorite_ads %}
**Unread messages:** {{ ns.unread_messages }}<br>
**Unread notifications:** {{ ns.unread_notifications }}
<details>
<summary><strong>Own ads ({{ own_ads | count }})</strong></summary>
{% if not own_ads_sensors %}
Own ads sensor not found.
{% elif own_ads %}
<ul>
{% for ad in own_ads %}
{% set ad_url = ad.url if ad.url and ad.url[:4] == 'http' else
'https://www.2dehands.be' ~ ad.url if ad.url else '' %}
{% set title = ad.name or ad.title or 'Ad' %}
<li>
<details>
<summary><strong>{% if ad_url %}<a href="{{ ad_url }}">{{ title | e }}</a>{% else %}{{ title | e }}{% endif %}</strong> - {{ ad.price or 'unknown' }}</summary>
{% if ad.image_url %}<p><img src="{{ ad.image_url }}" alt="{{ title | e }}" width="96"></p>{% else %}<p></p>{% endif %}
<ul>
<li><strong>Price:</strong> {{ ad.price or 'unknown' }}</li>
<li><strong>Status:</strong> {{ ad.status or 'unknown' }}</li>
<li><strong>Views:</strong> {{ ad.view_count if ad.view_count is not none else 'unknown' }}</li>
<li><strong>Needs extension:</strong> {{ 'yes' if ad.needs_extension else 'no' }}</li>
<li><strong>Can extend:</strong> {{ 'yes' if ad.can_extend else 'no' }}</li>
{% if ad.expiring %}<li><strong>Expiring:</strong> {{ 'yes' if ad.expiring else 'no' }}</li>
<li><strong>Extension pending:</strong> {{ 'yes' if ad.extension_pending else 'no' }}</li>
<li><strong>Expires at:</strong> {{ ad.expires_at or 'unknown' }}</li>{% else %}<p></p>{% endif %}
<li><strong>Id:</strong> {{ad.item_id }}</li>
</ul>
</details>
</li>
{% endfor %}
</ul>
{% else %}
No own ads found.
{% endif %}
</details>
<details>
<summary><strong>Favorite ads ({{ favorite_ads | count }})</strong></summary>
{% if not favorite_ads_sensors %}
Favorite ads sensor not found.
{% elif favorite_ads %}
<ul>
{% for ad in favorite_ads %}
{% set ad_url = ad.url if ad.url and ad.url[:4] == 'http' else
'https://www.2dehands.be' ~ ad.url if ad.url else '' %}
{% set title = ad.name or ad.title or 'Ad' %}
<li>
<details>
<summary><strong>{% if ad_url %}<a href="{{ ad_url }}">{{ title | e }}</a>{% else %}{{ title | e }}{% endif %}</strong> - {{ ad.price or 'unknown' }} - {{ ad.status or 'unknown' }}</summary>
{% if ad.image_url %}<p><img src="{{ ad.image_url }}" alt="{{ title | e }}" width="96"></p>{% endif %}
<ul>
<li><strong>Price:</strong> {{ ad.price or 'unknown' }}</li>
<li><strong>Status:</strong> {{ ad.status or 'unknown' }}</li>
<li><strong>Location:</strong> {{ ad.location or 'unknown' }}</li>
</ul>
</details>
</li>
{% endfor %}
</ul>
{% else %}
No favorite ads found.
{% endif %}
</details>
The built-in Home Assistant Markdown card cannot call services from a rendered button. To automatically create a button for every own ad that can be duplicated, install Auto Entities through HACS and add this manual card:
type: custom:auto-entities
card:
type: grid
title: Duplicate 2dehands ads
columns: 2
square: false
card_param: cards
filter:
include:
- domain: button
entity_id: "button.2dehands*_duplicate_*"
options:
type: button
entity: this.entity_id
icon: mdi:content-copy
show_state: false
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: this.entity_id
exclude:
- state: unavailable
sort:
method: name
ignore_case: true
show_empty: falseThe filter acts as the loop: it finds all generated duplicate button entities,
including buttons added after a website refresh, and creates one dashboard
button for each match. It also supports multiple configured 2dehands accounts.
Each button calls 2dehands.duplicate_ad with its matching source item ID.
Without the Auto Entities custom card, add the generated Duplicate ... button
entities manually to an Entities card from the dashboard editor.
Use the generated Refresh website data button entity when you want to fetch the latest website data without waiting for the hourly automatic refresh.
type: button
entity: button.2dehands_refresh_website_data
name: Refresh 2dehands
icon: mdi:refresh
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.2dehands_refresh_website_data