Skip to content
Merged
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
2 changes: 1 addition & 1 deletion homeassistant/components/blackbird/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pyblackbird"],
"quality_scale": "legacy",
"requirements": ["pyblackbird==0.6"]
"requirements": ["pyblackbird==0.10"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/blackbird/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import override

from pyblackbird import get_blackbird
from serial import SerialException
from serialx import SerialException
import voluptuous as vol

from homeassistant.components.media_player import (
Expand Down
93 changes: 48 additions & 45 deletions homeassistant/components/color_extractor/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Module for color_extractor (RGB extraction from images) component."""

import asyncio
from http import HTTPStatus
import io
import logging
from typing import Any
Expand All @@ -17,7 +18,7 @@
)
from homeassistant.const import SERVICE_TURN_ON
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import aiohttp_client, config_validation as cv

from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_GET_COLOR
Expand Down Expand Up @@ -67,32 +68,45 @@ def _get_color(file_handler: io.BytesIO | str) -> tuple[int, int, int]:

async def _async_extract_color_from_url(
hass: HomeAssistant, url: str
) -> tuple[int, int, int] | None:
) -> tuple[int, int, int]:
"""Handle call for URL based image."""
if not hass.config.is_allowed_external_url(url):
_LOGGER.error(
(
"External URL '%s' is not allowed, please add to"
" 'allowlist_external_urls'"
),
url,
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="url_not_allowed",
translation_placeholders={"url": url},
)
return None

_LOGGER.debug("Getting predominant RGB from image URL '%s'", url)

# Download the image into a buffer for ColorThief to check against
try:
session = aiohttp_client.async_get_clientsession(hass)

async with asyncio.timeout(10):
response = await session.get(url)

except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Failed to get ColorThief image due to HTTPError: %s", err)
return None

content = await response.content.read()
async with asyncio.timeout(10), session.get(url) as response:
if response.status != HTTPStatus.OK:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="http_error",
translation_placeholders={
"url": url,
"status": str(response.status),
},
)
content = await response.read()

except TimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout",
translation_placeholders={"url": url},
) from err
except aiohttp.ClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="fetch_failed",
translation_placeholders={"url": url, "error": str(err)},
) from err

with io.BytesIO(content) as _file:
_file.name = "color_extractor.jpg"
Expand All @@ -103,14 +117,14 @@ async def _async_extract_color_from_url(

def _extract_color_from_path(
hass: HomeAssistant, file_path: str
) -> tuple[int, int, int] | None:
) -> tuple[int, int, int]:
"""Handle call for local file based image."""
if not hass.config.is_allowed_path(file_path):
_LOGGER.error(
"File path '%s' is not allowed, please add to 'allowlist_external_dirs'",
file_path,
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="path_not_allowed",
translation_placeholders={"file_path": file_path},
)
return None

_LOGGER.debug("Getting predominant RGB from file path '%s'", file_path)

Expand All @@ -137,22 +151,21 @@ async def async_handle_service(service_call: ServiceCall) -> None:
_extract_color_from_path, service_call.hass, image_reference
)

# pylint: disable-next=home-assistant-action-swallowed-exception
except UnidentifiedImageError as ex:
_LOGGER.error(
"Bad image from %s '%s' provided, are you sure it's an image? %s",
image_type,
image_reference,
ex,
)
return
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_image",
translation_placeholders={
"image_type": image_type,
"image_reference": image_reference,
},
) from ex

if color:
service_data[ATTR_RGB_COLOR] = color
service_data[ATTR_RGB_COLOR] = color

await service_call.hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, blocking=True
)
await service_call.hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, blocking=True
)


async def async_handle_get_color(
Expand Down Expand Up @@ -186,16 +199,6 @@ async def async_handle_get_color(
},
) from ex

if color is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_image",
translation_placeholders={
"image_type": image_type,
"image_reference": image_reference,
},
)

return {"color": color}


Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/color_extractor/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,23 @@
}
},
"exceptions": {
"fetch_failed": {
"message": "Failed to fetch the image from {url}: {error}"
},
"http_error": {
"message": "Failed to fetch the image from {url}: the server responded with HTTP status {status}."
},
"invalid_image": {
"message": "Bad image {image_reference} from {image_type} provided, are you sure it's an image?"
},
"path_not_allowed": {
"message": "Path {file_path} is not allowed, add it to allowlist_external_dirs."
},
"timeout": {
"message": "Timed out fetching the image from {url}."
},
"url_not_allowed": {
"message": "URL {url} is not allowed, add it to allowlist_external_urls."
}
},
"services": {
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/easyenergy/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["easyenergy==3.0.1"],
"single_config_entry": true
}
100 changes: 100 additions & 0 deletions homeassistant/components/easyenergy/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-conditions:
status: exempt
comment: |
This integration does not provide custom conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: |
This integration does not provide custom triggers.
entity-event-setup:
status: exempt
comment: |
Entities in this integration do not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
The easyEnergy API does not require authentication.
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
This integration connects to a cloud service and does not support discovery.
discovery:
status: exempt
comment: |
This integration connects to a cloud service and does not support discovery.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices:
status: exempt
comment: |
This integration connects to a cloud service rather than physical devices.
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
This integration exposes a fixed set of service devices and does not discover,
add, or remove devices dynamically.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: exempt
comment: |
This integration has no user-configurable settings to reconfigure.
repair-issues:
status: exempt
comment: |
The integration has no user-actionable failure states that require a repair
issue.
stale-devices:
status: exempt
comment: |
This integration exposes a fixed set of service devices, so devices cannot
become stale after disappearing from the upstream service.

# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
56 changes: 54 additions & 2 deletions homeassistant/components/energyzero/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@

from typing import Any, override

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
import voluptuous as vol

from .const import DOMAIN
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.core import callback
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig

from .const import (
CONF_ELECTRICITY_PRICE_INTERVAL,
DEFAULT_ELECTRICITY_PRICE_INTERVAL,
DOMAIN,
ELECTRICITY_INTERVALS,
)


class EnergyZeroFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for EnergyZero integration."""

VERSION = 1

@staticmethod
@callback
@override
def async_get_options_flow(config_entry: ConfigEntry) -> EnergyZeroOptionsFlow:
"""Return the options flow."""
return EnergyZeroOptionsFlow()

@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
Expand All @@ -28,3 +49,34 @@ async def async_step_user(
title="EnergyZero",
data={},
)


class EnergyZeroOptionsFlow(OptionsFlowWithReload):
"""Manage EnergyZero options."""

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the electricity price interval."""
if user_input is not None:
return self.async_create_entry(data=user_input)

return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(
CONF_ELECTRICITY_PRICE_INTERVAL,
default=DEFAULT_ELECTRICITY_PRICE_INTERVAL,
): SelectSelector(
SelectSelectorConfig(
options=list(ELECTRICITY_INTERVALS),
translation_key=CONF_ELECTRICITY_PRICE_INTERVAL,
)
),
}
),
self.config_entry.options,
),
)
6 changes: 6 additions & 0 deletions homeassistant/components/energyzero/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import logging
from typing import Final

from energyzero import Interval

CONF_ELECTRICITY_PRICE_INTERVAL = "electricity_price_interval"
ELECTRICITY_INTERVALS = {"hourly": Interval.HOUR, "quarter_hourly": Interval.QUARTER}
DEFAULT_ELECTRICITY_PRICE_INTERVAL = "hourly"

DOMAIN: Final = "energyzero"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=10)
Expand Down
Loading
Loading