Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sensor, Make Sending Check Optional, Code Cleanup #46

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 0 additions & 30 deletions custom_components/healthchecksio/.translations/en.json

This file was deleted.

153 changes: 95 additions & 58 deletions custom_components/healthchecksio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,48 @@
https://github.com/custom-components/healthchecksio
"""
import asyncio
import json
import logging
import os
from datetime import timedelta

import async_timeout
import aiohttp
from homeassistant import config_entries, core
from homeassistant.const import Platform
from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
from integrationhelper import Logger
from integrationhelper.const import CC_STARTUP_VERSION

from .const import (
CONF_API_KEY,
CONF_CHECK,
CONF_CREATE_BINARY_SENSOR,
CONF_CREATE_SENSOR,
CONF_PING_ENDPOINT,
CONF_SELF_HOSTED,
CONF_SITE_ROOT,
DATA_CLIENT,
DATA_DATA,
DOMAIN,
DOMAIN_DATA,
INTEGRATION_NAME,
INTEGRATION_VERSION,
ISSUE_URL,
OFFICIAL_SITE_ROOT,
REQUIRED_FILES,
)

_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)


async def async_setup(hass: core.HomeAssistant, config: ConfigType):
async def async_setup(
hass: core.HomeAssistant, config: config_entries.ConfigEntry
) -> bool:
"""Set up this component using YAML is not supported."""
if config.get(DOMAIN) is not None:
Logger("custom_components.healthchecksio").error(
"Configuration with YAML is not supported"
)
_LOGGER.error("Configuration with YAML is not supported")

return True

Expand All @@ -44,9 +56,9 @@ async def async_setup_entry(
) -> bool:
"""Set up this integration using UI."""
# Print startup message
Logger("custom_components.healthchecksio").info(
_LOGGER.info(
CC_STARTUP_VERSION.format(
name=DOMAIN, version=INTEGRATION_VERSION, issue_link=ISSUE_URL
name=INTEGRATION_NAME, version=INTEGRATION_VERSION, issue_link=ISSUE_URL
)
)

Expand All @@ -58,27 +70,27 @@ async def async_setup_entry(
# Create DATA dict
if DOMAIN_DATA not in hass.data:
hass.data[DOMAIN_DATA] = {}
if "data" not in hass.data[DOMAIN_DATA]:
hass.data[DOMAIN_DATA] = {}
if DATA_DATA not in hass.data[DOMAIN_DATA]:
hass.data[DOMAIN_DATA][DATA_DATA] = {}

# Get "global" configuration.
api_key = config_entry.data.get("api_key")
check = config_entry.data.get("check")
self_hosted = config_entry.data.get("self_hosted")
site_root = config_entry.data.get("site_root")
ping_endpoint = config_entry.data.get("ping_endpoint")
api_key = config_entry.data.get(CONF_API_KEY)
check = config_entry.data.get(CONF_CHECK)
self_hosted = config_entry.data.get(CONF_SELF_HOSTED)
site_root = config_entry.data.get(CONF_SITE_ROOT)
ping_endpoint = config_entry.data.get(CONF_PING_ENDPOINT)
platforms = []
if config_entry.data.get(CONF_CREATE_BINARY_SENSOR):
platforms.append(Platform.BINARY_SENSOR)
if config_entry.data.get(CONF_CREATE_SENSOR):
platforms.append(Platform.SENSOR)

# Configure the client.
hass.data[DOMAIN_DATA]["client"] = HealthchecksioData(
hass.data[DOMAIN_DATA][DATA_CLIENT] = HealthchecksioData(
hass, api_key, check, self_hosted, site_root, ping_endpoint
)

# Add binary_sensor
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(
config_entry, Platform.BINARY_SENSOR
)
)
await hass.config_entries.async_forward_entry_setups(config_entry, platforms)
_LOGGER.debug(f"Config Entry: {config_entry.as_dict()}")

return True

Expand All @@ -87,15 +99,26 @@ async def async_unload_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
) -> bool:
"""Unload a config entry."""

unload_ok = await hass.config_entries.async_forward_entry_unload(
config_entry, Platform.BINARY_SENSOR
)
_LOGGER.debug(f"Unloading Config Entry: {config_entry.as_dict()}")
curr_plat = []
for p in entity_platform.async_get_platforms(hass, DOMAIN):
if (
p.config_entry is not None
and config_entry.entry_id == p.config_entry.entry_id
and p.config_entry.state == config_entries.ConfigEntryState.LOADED
):
curr_plat.append(p.domain)
_LOGGER.debug(f"Unloading Platforms: {curr_plat}")
unload_ok = True
if curr_plat:
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry,
curr_plat,
)
if unload_ok:
hass.data.pop(DOMAIN_DATA, None)
Logger("custom_components.healthchecksio").info(
"Successfully removed the healthchecksio integration"
)
_LOGGER.info("Successfully removed the HealthChecks.io integration")

return unload_ok


Expand All @@ -114,28 +137,47 @@ def __init__(self, hass, api_key, check, self_hosted, site_root, ping_endpoint):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update_data(self):
"""Update data."""
Logger("custom_components.healthchecksio").debug("Running update")
_LOGGER.debug("Running Update")
# This is where the main logic to update platform data goes.
try:
verify_ssl = not self.self_hosted or self.site_root.startswith("https")
session = async_get_clientsession(self.hass, verify_ssl)
headers = {"X-Api-Key": self.api_key}
async with async_timeout.timeout(10):
data = await session.get(
f"{self.site_root}/api/v1/checks/", headers=headers
)
self.hass.data[DOMAIN_DATA]["data"] = await data.json()

if self.self_hosted:
check_url = f"{self.site_root}/{self.ping_endpoint}/{self.check}"
verify_ssl = not self.self_hosted or self.site_root.startswith("https")
session = async_get_clientsession(self.hass, verify_ssl)
timeout10 = aiohttp.ClientTimeout(total=10)
headers = {"X-Api-Key": self.api_key}
if self.check is not None:
if self.self_hosted:
check_url = f"{self.site_root}/{self.ping_endpoint}/{self.check}"
else:
check_url = f"https://hc-ping.com/{self.check}"
await asyncio.sleep(1) # needed for self-hosted instances
try:
check_response = await session.get(check_url, timeout=timeout10)
except (aiohttp.ClientError, asyncio.TimeoutError) as error:
_LOGGER.error(f"Could Not Send Check: {error}")
else:
if check_response.ok:
_LOGGER.debug(
f"Send Check HTTP Status Code: {check_response.status}"
)
else:
check_url = f"https://hc-ping.com/{self.check}"
await asyncio.sleep(1) # needed for self-hosted instances
await session.get(check_url)
except Exception as error: # pylint: disable=broad-except
Logger("custom_components.healthchecksio").error(
f"Could not update data - {error}"
_LOGGER.error(
f"Error: Send Check HTTP Status Code: {check_response.status}"
)
else:
_LOGGER.debug("Send Check is not defined")
try:
data = await session.get(
f"{self.site_root}/api/v1/checks/", headers=headers, timeout=timeout10
)
self.hass.data[DOMAIN_DATA][DATA_DATA] = await data.json()
except (aiohttp.ClientError, asyncio.TimeoutError) as error:
_LOGGER.error(f"Could Not Update Data: {error}")
except (ValueError, json.decoder.JSONDecodeError) as error:
_LOGGER.error(f"Data JSON Decode Error: {error}")
else:
if data.ok:
_LOGGER.debug(f"Get Data HTTP Status Code: {data.status}")
else:
_LOGGER.error(f"Error: Get Data HTTP Status Code: {data.status}")


async def check_files(hass: core.HomeAssistant) -> bool:
Expand All @@ -149,11 +191,6 @@ async def check_files(hass: core.HomeAssistant) -> bool:
missing.append(file)

if missing:
Logger("custom_components.healthchecksio").critical(
f"The following files are missing: {missing}"
)
returnvalue = False
else:
returnvalue = True

return returnvalue
_LOGGER.critical(f"The following files are missing: {missing}")
return False
return True
100 changes: 46 additions & 54 deletions custom_components/healthchecksio/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
"""Binary sensor platform for Healthchecksio."""
try:
from homeassistant.components.binary_sensor import BinarySensorEntity
except ImportError:
from homeassistant.components.binary_sensor import (
BinarySensorDevice as BinarySensorEntity,
)

from .const import ATTRIBUTION, BINARY_SENSOR_DEVICE_CLASS, DOMAIN_DATA, DOMAIN
import logging

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)

from .const import (
ATTR_ATTRIBUTION,
ATTR_CHECKS,
ATTR_LAST_PING,
ATTR_NAME,
ATTR_PING_URL,
ATTR_STATUS,
ATTRIBUTION,
DATA_CLIENT,
DATA_DATA,
DOMAIN,
DOMAIN_DATA,
)

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry, async_add_devices):
"""Setup sensor platform."""
# Send update "signal" to the component
await hass.data[DOMAIN_DATA]["client"].update_data()
"""Setup Binary Sensor platform."""
await hass.data[DOMAIN_DATA][DATA_CLIENT].update_data()
checks = []
for check in hass.data[DOMAIN_DATA].get("data", {}).get("checks", []):
for check in hass.data[DOMAIN_DATA].get(DATA_DATA, {}).get(ATTR_CHECKS, []):
check_data = {
"name": check.get("name"),
"last_ping": check.get("last_ping"),
"status": check.get("status"),
"ping_url": check.get("ping_url"),
ATTR_NAME: check.get(ATTR_NAME),
ATTR_LAST_PING: check.get(ATTR_LAST_PING),
ATTR_STATUS: check.get(ATTR_STATUS),
ATTR_PING_URL: check.get(ATTR_PING_URL),
}
checks.append(HealthchecksioBinarySensor(hass, check_data, config_entry))
async_add_devices(checks, True)
Expand All @@ -30,57 +44,35 @@ class HealthchecksioBinarySensor(BinarySensorEntity):

def __init__(self, hass, check_data, config_entry):
self.hass = hass
self.attr = {}
self.config_entry = config_entry
self._status = None
self.check_data = check_data
self._attr_name = None
self._attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
self._attr_unique_id = self.check_data.get(ATTR_PING_URL, "").split("/")[-1]
self._attr_extra_state_attributes = {}
self._attr_is_on = None
self.check = {}

async def async_update(self):
"""Update the binary_sensor."""
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()

# Check the data and update the value.
for check in self.hass.data[DOMAIN_DATA].get("data", {}).get("checks", []):
if self.unique_id == check.get("ping_url").split("/")[-1]:
await self.hass.data[DOMAIN_DATA][DATA_CLIENT].update_data()
for check in (
self.hass.data[DOMAIN_DATA].get(DATA_DATA, {}).get(ATTR_CHECKS, [])
):
if self._attr_unique_id == check.get(ATTR_PING_URL).split("/")[-1]:
self.check = check
break
self._status = self.check.get("status") != "down"

# Set/update attributes
self.attr["attribution"] = ATTRIBUTION
self.attr["last_ping"] = self.check.get("last_ping")

@property
def unique_id(self):
"""Return a unique ID to use for this binary_sensor."""
return self.check_data.get("ping_url", "").split("/")[-1]
self._attr_name = self.check.get(ATTR_NAME)
self._attr_is_on = self.check.get(ATTR_STATUS) != "down"
self._attr_extra_state_attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
self._attr_extra_state_attributes[ATTR_LAST_PING] = self.check.get(
ATTR_LAST_PING
)

@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.config_entry.entry_id)},
"name": "Healthchecks.io",
"name": "HealthChecks.io",
"manufacturer": "SIA Monkey See Monkey Do",
}

@property
def name(self):
"""Return the name of the binary_sensor."""
return self.check.get("name")

@property
def device_class(self):
"""Return the class of this binary_sensor."""
return BINARY_SENSOR_DEVICE_CLASS

@property
def is_on(self):
"""Return true if the binary_sensor is on."""
return self._status

@property
def extra_state_attributes(self):
"""Return the state attributes."""
return self.attr
Loading
Loading