Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.37.4
uses: github/codeql-action/init@v4.38.1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -65,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4.37.4
uses: github/codeql-action/autobuild@v4.38.1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -78,4 +78,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.37.4
uses: github/codeql-action/analyze@v4.38.1
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
uses: actions/checkout@v7

- name: Login to Docker Hub
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4.6.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_API_KEY }}
Expand Down
41 changes: 28 additions & 13 deletions custom_components/uk_bin_collection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
LOG_PREFIX,
PLATFORMS,
EXCLUDED_ARG_KEYS,
redact_config_data,
)
from uk_bin_collection.uk_bin_collection.collect_data import UKBinCollectionApp

Expand All @@ -34,7 +35,11 @@

async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the UK Bin Collection component."""
_LOGGER.debug(f"{LOG_PREFIX} async_setup called with config: {config}")
_LOGGER.debug(
"%s async_setup called with config: %s",
LOG_PREFIX,
redact_config_data(config),
)
try:
hass.data.setdefault(DOMAIN, {})
_LOGGER.debug(
Expand Down Expand Up @@ -185,15 +190,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
# to True as a safe fallback: if the flag is somehow missing we prefer
# automatic polling over silently leaving the sensor stale.
auto_refresh_enabled = config_entry.data.get("auto_refresh_enabled", True)
icon_color_mapping = config_entry.data.get("icon_color_mapping", "{}")
update_interval_hours = config_entry.data.get("update_interval", 12)

_LOGGER.debug(
f"{LOG_PREFIX} Retrieved configuration: "
f"name={name}, timeout={timeout}, "
f"configuration={redact_config_data(config_entry.data)}, "
f"timeout={timeout}, "
f"auto_refresh_enabled={auto_refresh_enabled}, "
f"update_interval={update_interval_hours} hours, "
f"icon_color_mapping={icon_color_mapping}"
f"update_interval={update_interval_hours} hours"
)

# Validate 'timeout'
Expand Down Expand Up @@ -232,7 +236,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b

# Prepare arguments for UKBinCollectionApp
args = build_ukbcd_args(config_entry.data)
_LOGGER.debug(f"{LOG_PREFIX} UKBinCollectionApp args: {args}")
_LOGGER.debug(
"%s UKBinCollectionApp arguments prepared (%d values).",
LOG_PREFIX,
len(args),
)

# Initialize the UK Bin Collection Data application
ukbcd = UKBinCollectionApp()
Expand Down Expand Up @@ -394,7 +402,10 @@ def __init__(
self._last_good_data = {}

_LOGGER.debug(
f"{LOG_PREFIX} HouseholdBinCoordinator __init__: name={name}, timeout={timeout}, update_interval={update_interval}"
"%s HouseholdBinCoordinator initialized (timeout=%s, update_interval=%s).",
LOG_PREFIX,
timeout,
update_interval,
)

async def _async_update_data(self) -> dict:
Expand All @@ -409,10 +420,10 @@ async def _async_update_data(self) -> dict:
self.hass.async_add_executor_job(self.ukbcd.run),
timeout=self.timeout,
)
_LOGGER.debug(f"{LOG_PREFIX} Raw data fetched from ukbcd.run(): {data}")
_LOGGER.debug("%s Raw bin data fetched from ukbcd.run().", LOG_PREFIX)

parsed_data = json.loads(data)
_LOGGER.debug(f"{LOG_PREFIX} JSON parsed data: {parsed_data}")
_LOGGER.debug("%s JSON response parsed successfully.", LOG_PREFIX)

processed_data = self.process_bin_data(parsed_data)

Expand All @@ -427,7 +438,11 @@ async def _async_update_data(self) -> dict:
return {}

self._last_good_data = processed_data
_LOGGER.debug(f"{LOG_PREFIX} Processed data: {processed_data}")
_LOGGER.debug(
"%s Processed %d bin collection entries.",
LOG_PREFIX,
len(processed_data),
)

_LOGGER.info(f"{LOG_PREFIX} Bin collection data updated successfully.")
return processed_data
Expand All @@ -445,17 +460,17 @@ async def _async_update_data(self) -> dict:
@staticmethod
def process_bin_data(data: dict) -> dict:
"""Process raw data to determine the next collection dates."""
_LOGGER.debug(f"{LOG_PREFIX} process_bin_data called with data={data}")
_LOGGER.debug("%s Processing bin collection response.", LOG_PREFIX)

current_date = dt_util.now().date()
next_collection_dates = {}

bins = data.get("bins", [])
_LOGGER.debug(f"{LOG_PREFIX} Bins found: {bins}")
_LOGGER.debug("%s Bins found: %d", LOG_PREFIX, len(bins))
for bin_data in bins:
bin_type = bin_data.get("type")
collection_date_str = bin_data.get("collectionDate")
_LOGGER.debug(f"{LOG_PREFIX} Processing bin_data={bin_data}")
_LOGGER.debug("%s Processing a bin collection entry.", LOG_PREFIX)

if not bin_type or not collection_date_str:
_LOGGER.warning(
Expand Down
33 changes: 22 additions & 11 deletions custom_components/uk_bin_collection/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SELENIUM_SERVER_URLS,
BROWSER_BINARIES,
INPUT_JSON_URL,
redact_config_data,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -67,7 +68,7 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
_LOGGER.debug("Loaded council data: %s", self.council_names)

if user_input is not None:
_LOGGER.debug("User input received: %s", user_input)
_LOGGER.debug("User input received: %s", redact_config_data(user_input))
# Validate user input
if not user_input.get("name"):
errors["name"] = "Name is required."
Expand All @@ -85,7 +86,8 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
if existing_entry:
errors["base"] = "duplicate_entry"
_LOGGER.warning(
"Duplicate entry found: %s", existing_entry.data.get("name")
"Duplicate entry found: %s",
redact_config_data(existing_entry.data).get("name"),
)

if not errors:
Expand All @@ -106,7 +108,9 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
user_input["council"] = council_key
self.data.update(user_input)

_LOGGER.debug("User input after mapping: %s", self.data)
_LOGGER.debug(
"User input after mapping: %s", redact_config_data(self.data)
)

# Proceed to the council step
return await self.async_step_council()
Expand Down Expand Up @@ -134,7 +138,7 @@ async def async_step_council(self, user_input: Optional[Dict[str, Any]] = None):
requires_selenium = "web_driver" in council_info

if user_input is not None:
_LOGGER.debug("Council step user input: %s", user_input)
_LOGGER.debug("Council step user input: %s", redact_config_data(user_input))
# Validate JSON mapping if provided
if user_input.get("icon_color_mapping"):
if not self.is_valid_json(user_input["icon_color_mapping"]):
Expand All @@ -151,7 +155,9 @@ async def async_step_council(self, user_input: Optional[Dict[str, Any]] = None):
# If no errors, create the config entry
if not errors:
_LOGGER.info(
"%s Creating config entry with data: %s", LOG_PREFIX, self.data
"%s Creating config entry with data: %s",
LOG_PREFIX,
redact_config_data(self.data),
)
return self.async_create_entry(title=self.data["name"], data=self.data)
else:
Expand Down Expand Up @@ -202,7 +208,7 @@ async def async_step_reconfigure_confirm(
council_wiki_name = council_info.get("wiki_name", "")

if user_input is not None:
_LOGGER.debug("Reconfigure user input: %s", user_input)
_LOGGER.debug("Reconfigure user input: %s", redact_config_data(user_input))
# Map selected wiki_name back to council key
council_key = self.map_wiki_name_to_council_key(user_input["council"])
user_input["council"] = council_key
Expand Down Expand Up @@ -403,20 +409,25 @@ async def check_selenium_server(self, custom_url: Optional[str] = None) -> list:
results = []
async with aiohttp.ClientSession() as session:
for url in urls:
redacted_url = redact_config_data({"selenium_url": url})["selenium_url"]
try:
async with session.get(url, timeout=5) as response:
response.raise_for_status()
accessible = response.status == 200
results.append((url, accessible))
_LOGGER.debug("Selenium server %s is accessible.", url)
_LOGGER.debug("Selenium server %s is accessible.", redacted_url)
except aiohttp.ClientError as e:
_LOGGER.warning(
"Failed to connect to Selenium server at %s: %s", url, e
"Failed to connect to Selenium server at %s: %s",
redacted_url,
type(e).__name__,
)
results.append((url, False))
except Exception as e:
_LOGGER.exception(
"Unexpected error checking Selenium server at %s: %s", url, e
_LOGGER.warning(
"Unexpected error checking Selenium server at %s: %s",
redacted_url,
type(e).__name__,
)
results.append((url, False))
return results
Expand Down Expand Up @@ -531,7 +542,7 @@ async def async_step_init(self, user_input=None):
_LOGGER.debug("Loaded council data for options flow.")

if user_input is not None:
_LOGGER.debug("Options flow user input: %s", user_input)
_LOGGER.debug("Options flow user input: %s", redact_config_data(user_input))
# Map selected wiki_name back to council key
council_key = self.map_wiki_name_to_council_key(user_input["council"])
user_input["council"] = council_key
Expand Down
27 changes: 27 additions & 0 deletions custom_components/uk_bin_collection/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,30 @@
"auto_refresh_enabled",
"original_parser",
}

# These values can identify a household, disclose a private endpoint, or
# contain credentials. Keep them out of Home Assistant logs, which are often
# collected for diagnostics and shared outside the local installation.
SENSITIVE_CONFIG_KEYS = frozenset(
{
"name",
"url",
"uprn",
"postcode",
"number",
"usrn",
"web_driver",
"selenium_url",
"icon_color_mapping",
"password",
"token",
}
)


def redact_config_data(data: dict) -> dict:
"""Return a shallow copy suitable for diagnostic logging."""
return {
key: "<redacted>" if key in SENSITIVE_CONFIG_KEYS else value
for key, value in data.items()
}
Loading
Loading