Skip to content

Commit

Permalink
Merge pull request #912 from robbrad/feature/MoveToDeviceAndSensors
Browse files Browse the repository at this point in the history
Feature/move to device and sensors
  • Loading branch information
robbrad authored Oct 20, 2024
2 parents e345c83 + 41d02b3 commit e5583d0
Show file tree
Hide file tree
Showing 19 changed files with 334 additions and 196 deletions.
10 changes: 7 additions & 3 deletions custom_components/uk_bin_collection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info(LOG_PREFIX + "Data Supplied: %s", entry.data)

council_name = entry.data.get("council", "Unknown Council")
_LOGGER.info(LOG_PREFIX + "Setting up UK Bin Collection Data for council: %s", council_name)
_LOGGER.info(
LOG_PREFIX + "Setting up UK Bin Collection Data for council: %s", council_name
)

hass.data.setdefault(DOMAIN, {})

Expand All @@ -25,11 +27,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Forward the entry setup to the sensor platform
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

_LOGGER.info(LOG_PREFIX + "Successfully set up UK Bin Collection Data for council: %s", council_name)
_LOGGER.info(
LOG_PREFIX + "Successfully set up UK Bin Collection Data for council: %s",
council_name,
)
return True



async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
7 changes: 5 additions & 2 deletions custom_components/uk_bin_collection/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ async def async_step_user(self, user_input=None):
{
vol.Required("name", default=""): cv.string,
vol.Required("council", default=""): vol.In(self.council_options),
vol.Optional("icon_color_mapping", default=""): cv.string, # Optional field
vol.Optional(
"icon_color_mapping", default=""
): cv.string, # Optional field
}
),
errors=errors,
Expand Down Expand Up @@ -279,7 +281,8 @@ async def async_step_reconfigure_confirm(
schema = schema.extend(
{
vol.Optional(
"icon_color_mapping", default=existing_data.get("icon_color_mapping", "")
"icon_color_mapping",
default=existing_data.get("icon_color_mapping", ""),
): str
}
)
Expand Down
100 changes: 84 additions & 16 deletions custom_components/uk_bin_collection/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,18 @@ async def async_setup_entry(hass, config, async_add_entities):

name = config.data.get("name", "")
timeout = config.data.get("timeout", 60)
icon_color_mapping = config.data.get("icon_color_mapping", "{}") # Use an empty JSON object as default
icon_color_mapping = config.data.get(
"icon_color_mapping", "{}"
) # Use an empty JSON object as default

args = [
config.data.get("council", ""),
config.data.get("url", ""),
*(
f"--{key}={value}"
for key, value in config.data.items()
if key not in {
if key
not in {
"name",
"council",
"url",
Expand Down Expand Up @@ -87,7 +90,9 @@ async def async_setup_entry(hass, config, async_add_entities):
for bin_type in coordinator.data.keys():
device_id = f"{name}_{bin_type}"
entities.append(
UKBinCollectionDataSensor(coordinator, bin_type, device_id, icon_color_mapping)
UKBinCollectionDataSensor(
coordinator, bin_type, device_id, icon_color_mapping
)
)
entities.append(
UKBinCollectionAttributeSensor(
Expand Down Expand Up @@ -140,8 +145,12 @@ async def async_setup_entry(hass, config, async_add_entities):
)
)

# Add the new Raw JSON Sensor
entities.append(UKBinCollectionRawJSONSensor(coordinator, f"{name}_raw_json", name))

async_add_entities(entities)


class HouseholdBinCoordinator(DataUpdateCoordinator):
"""Household Bin Coordinator"""

Expand All @@ -168,15 +177,17 @@ def get_latest_collection_info(data) -> dict:
"""Process the bin collection data."""
current_date = datetime.now()
next_collection_dates = {}

for bin_data in data["bins"]:
bin_type = bin_data["type"]
collection_date_str = bin_data["collectionDate"]
collection_date = datetime.strptime(collection_date_str, "%d/%m/%Y")

if collection_date.date() >= current_date.date():
if bin_type in next_collection_dates:
if collection_date < datetime.strptime(next_collection_dates[bin_type], "%d/%m/%Y"):
if collection_date < datetime.strptime(
next_collection_dates[bin_type], "%d/%m/%Y"
):
next_collection_dates[bin_type] = collection_date_str
else:
next_collection_dates[bin_type] = collection_date_str
Expand All @@ -190,12 +201,16 @@ class UKBinCollectionDataSensor(CoordinatorEntity, SensorEntity):

device_class = DEVICE_CLASS

def __init__(self, coordinator, bin_type, device_id, icon_color_mapping=None) -> None:
def __init__(
self, coordinator, bin_type, device_id, icon_color_mapping=None
) -> None:
"""Initialize the main bin sensor."""
super().__init__(coordinator)
self._bin_type = bin_type
self._device_id = device_id
self._icon_color_mapping = json.loads(icon_color_mapping) if icon_color_mapping else {}
self._icon_color_mapping = (
json.loads(icon_color_mapping) if icon_color_mapping else {}
)
self.apply_values()

@property
Expand Down Expand Up @@ -270,6 +285,7 @@ def extra_state_attributes(self):
STATE_ATTR_NEXT_COLLECTION: self._next_collection.strftime("%d/%m/%Y"),
STATE_ATTR_DAYS: self._days,
}

@property
def color(self):
"""Return the entity icon."""
Expand All @@ -284,14 +300,24 @@ def unique_id(self):
class UKBinCollectionAttributeSensor(CoordinatorEntity, SensorEntity):
"""Implementation of the attribute sensors (Colour, Next Collection, Days, Bin Type, Raw Next Collection)."""

def __init__(self, coordinator, bin_type, unique_id, attribute_type, device_id, icon_color_mapping=None) -> None:
def __init__(
self,
coordinator,
bin_type,
unique_id,
attribute_type,
device_id,
icon_color_mapping=None,
) -> None:
"""Initialize the attribute sensor."""
super().__init__(coordinator)
self._bin_type = bin_type
self._unique_id = unique_id
self._attribute_type = attribute_type
self._device_id = device_id
self._icon_color_mapping = json.loads(icon_color_mapping) if icon_color_mapping else {}
self._icon_color_mapping = (
json.loads(icon_color_mapping) if icon_color_mapping else {}
)

# Use user-supplied icon and color if available
self._icon = self._icon_color_mapping.get(self._bin_type, {}).get("icon")
Expand Down Expand Up @@ -320,14 +346,20 @@ def state(self):
if self._attribute_type == "Colour":
return self._color # Return the colour of the bin
elif self._attribute_type == "Next Collection Human Readable":
return self.coordinator.data[self._bin_type] # Already formatted next collection
return self.coordinator.data[
self._bin_type
] # Already formatted next collection
elif self._attribute_type == "Days Until Collection":
next_collection = parser.parse(self.coordinator.data[self._bin_type], dayfirst=True).date()
next_collection = parser.parse(
self.coordinator.data[self._bin_type], dayfirst=True
).date()
return (next_collection - datetime.now().date()).days
elif self._attribute_type == "Bin Type":
return self._bin_type # Return the bin type for the Bin Type sensor
elif self._attribute_type == "Next Collection Date":
return self.coordinator.data[self._bin_type] # Return the raw next collection date
return self.coordinator.data[
self._bin_type
] # Return the raw next collection date

@property
def icon(self):
Expand All @@ -344,14 +376,18 @@ def extra_state_attributes(self):
"""Return extra attributes of the sensor."""
return {
STATE_ATTR_COLOUR: self._color,
STATE_ATTR_NEXT_COLLECTION: self.coordinator.data[self._bin_type], # Return the collection date
STATE_ATTR_NEXT_COLLECTION: self.coordinator.data[
self._bin_type
], # Return the collection date
}

@property
def device_info(self):
"""Return device information for grouping sensors."""
return {
"identifiers": {(DOMAIN, self._device_id)}, # Use the same device_id for all sensors of the same bin type
"identifiers": {
(DOMAIN, self._device_id)
}, # Use the same device_id for all sensors of the same bin type
"name": f"{self.coordinator.name} {self._bin_type}",
"manufacturer": "UK Bin Collection",
"model": "Bin Sensor",
Expand All @@ -361,4 +397,36 @@ def device_info(self):
@property
def unique_id(self):
"""Return a unique ID for the sensor."""
return self._unique_id
return self._unique_id


class UKBinCollectionRawJSONSensor(CoordinatorEntity, SensorEntity):
"""Sensor to hold the raw JSON data for bin collections."""

def __init__(self, coordinator, unique_id, name) -> None:
"""Initialize the raw JSON sensor."""
super().__init__(coordinator)
self._unique_id = unique_id
self._name = name

@property
def name(self):
"""Return the name of the sensor."""
return f"{self._name} Raw JSON"

@property
def state(self):
"""Return the state, which is the raw JSON data."""
return json.dumps(self.coordinator.data) # Convert the raw dict to JSON string

@property
def unique_id(self):
"""Return a unique ID for the sensor."""
return self._unique_id

@property
def extra_state_attributes(self):
"""Return extra attributes for the sensor."""
return {
"raw_data": self.coordinator.data # Provide the raw data as an attribute
}
2 changes: 1 addition & 1 deletion uk_bin_collection/tests/input.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@
},
"EastDevonDC": {
"url": "https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN=010090909915",
"wiki_command_url_override": "https://eastdevon.gov.uk/recycling-waste/recycling-and-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN=XXXXXXXX",
"wiki_command_url_override": "https://eastdevon.gov.uk/recycling-and-waste/recycling-waste-information/when-is-my-bin-collected/future-collections-calendar/?UPRN=XXXXXXXX",
"wiki_name": "East Devon District Council",
"wiki_note": "Replace XXXXXXXX with UPRN."
},
Expand Down
81 changes: 42 additions & 39 deletions uk_bin_collection/uk_bin_collection/councils/BasildonCouncil.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from uk_bin_collection.uk_bin_collection.common import *
import requests
import json
from datetime import datetime
from uk_bin_collection.uk_bin_collection.common import check_uprn, date_format as DATE_FORMAT
from uk_bin_collection.uk_bin_collection.get_bin_data import AbstractGetBinDataClass


# import the wonderful Beautiful Soup and the URL grabber
class CouncilClass(AbstractGetBinDataClass):
"""
Concrete classes have to implement all abstract operations of the
base class. They can also override some operations with a default
implementation.
Concrete class that implements the abstract bin data fetching and parsing logic.
"""

def parse_data(self, page: str, **kwargs) -> dict:
Expand All @@ -18,64 +18,67 @@ def parse_data(self, page: str, **kwargs) -> dict:
check_uprn(uprn)

payload = {
# Add your payload details here (replace this with the actual payload structure if required)
"uprn": uprn
}

# Headers for the request
headers = {
"Content-Type": "application/json"
}
headers = {"Content-Type": "application/json"}

response = requests.post(url_base, data=json.dumps(payload), headers=headers)

# Ensure the request was successful
if response.status_code == 200:
data = response.json()

# Initialize an empty list to store the bin collection details

bins = []

# Function to add collection details to bins list
def add_collection(service_name, collection_data):
bins.append({
"type": service_name,
"collectionDate": collection_data.get("current_collection_date")
})
bins.append(
{
"type": service_name,
"collectionDate": collection_data.get("current_collection_date"),
}
)

# Extract refuse information
available_services = data["refuse"]["available_services"]
available_services = data.get("refuse", {}).get("available_services", {})

date_format = "%d-%m-%Y" # Define the desired date format

for service_name, service_data in available_services.items():
# Append the service name and current collection date to the "bins" list
# Handle the different cases of service data
match service_data["container"]:
case "Green Wheelie Bin":
subscription_status = service_data["subscription"]["active"] if service_data["subscription"] else False
type_descr = f"Green Wheelie Bin ({"Active" if subscription_status else "Expired"})"
subscription_status = (
service_data["subscription"]["active"]
if service_data.get("subscription")
else False
)
type_descr = f"Green Wheelie Bin ({'Active' if subscription_status else 'Expired'})"
case "N/A":
type_descr = service_data["name"]
type_descr = service_data.get("name", "Unknown Service")
case _:
type_descr = service_data["container"]

type_descr = service_data.get("container", "Unknown Container")

date_str = service_data.get("current_collection_date")
# Parse the date string into a datetime object
date_obj = datetime.strptime(date_str, "%Y-%m-%d")

# Convert the datetime object to the desired format
formatted_date = date_obj.strftime(date_format)

bins.append({
"type": type_descr, # Use service name from the data
"collectionDate": formatted_date
})
if date_str: # Ensure the date string exists
try:
# Parse and format the date string
date_obj = datetime.strptime(date_str, "%Y-%m-%d")
formatted_date = date_obj.strftime(DATE_FORMAT)
except ValueError:
formatted_date = "Invalid Date"
else:
formatted_date = "No Collection Date"

bins.append(
{
"type": type_descr, # Use service name from the data
"collectionDate": formatted_date,
}
)

else:
print(f"Failed to fetch data. Status code: {response.status_code}")
return {}

data = {
"bins": bins
}

return data
return {"bins": bins}
Loading

0 comments on commit e5583d0

Please sign in to comment.