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 Dockerfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion homeassistant/components/go2rtc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
RECOMMENDED_VERSION = "1.9.9"
RECOMMENDED_VERSION = "1.9.11"
1 change: 1 addition & 0 deletions homeassistant/components/goodwe/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ async def async_setup_entry(
class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
"""Entity representing individual inverter sensor."""

_attr_has_entity_name = True
entity_description: GoodweSensorEntityDescription

def __init__(
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/sql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
DOMAIN,
PLATFORMS,
)
from .services import async_setup_services
from .util import redact_credentials, validate_sql_select

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -71,6 +72,8 @@

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up SQL from yaml config."""
async_setup_services(hass)

if (conf := config.get(DOMAIN)) is None:
return True

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/sql/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"services": {
"query": {
"service": "mdi:database-search"
}
}
}
131 changes: 131 additions & 0 deletions homeassistant/components/sql/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Services for the SQL integration."""

from __future__ import annotations

import datetime
import decimal
import logging

from sqlalchemy.engine import Result
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
import voluptuous as vol

from homeassistant.components.recorder import CONF_DB_URL, get_instance
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.util.json import JsonValueType

from .const import CONF_QUERY, DOMAIN
from .util import (
async_create_sessionmaker,
generate_lambda_stmt,
redact_credentials,
resolve_db_url,
validate_query,
validate_sql_select,
)

_LOGGER = logging.getLogger(__name__)

SERVICE_QUERY = "query"
SERVICE_QUERY_SCHEMA = vol.Schema(
{
vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select),
vol.Optional(CONF_DB_URL): cv.string,
}
)


async def _async_query_service(
call: ServiceCall,
) -> ServiceResponse:
"""Execute a SQL query service and return the result."""
db_url = resolve_db_url(call.hass, call.data.get(CONF_DB_URL))
query_str = call.data[CONF_QUERY]
(
sessmaker,
uses_recorder_db,
use_database_executor,
) = await async_create_sessionmaker(call.hass, db_url)
try:
validate_query(call.hass, query_str, uses_recorder_db, None)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="query_not_allowed",
translation_placeholders={"error": str(err)},
) from err
if sessmaker is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="db_connection_failed",
translation_placeholders={"db_url": redact_credentials(db_url)},
)

def _execute_and_convert_query() -> list[JsonValueType]:
"""Execute the query and return the results with converted types."""
sess: Session = sessmaker()
try:
result: Result = sess.execute(generate_lambda_stmt(query_str))
except SQLAlchemyError as err:
_LOGGER.debug(
"Error executing query %s: %s",
query_str,
redact_credentials(str(err)),
)
sess.rollback()
raise
else:
rows: list[JsonValueType] = []
for row in result.mappings():
processed_row: dict[str, JsonValueType] = {}
for key, value in row.items():
if isinstance(value, decimal.Decimal):
processed_row[key] = float(value)
elif isinstance(value, datetime.date):
processed_row[key] = value.isoformat()
elif isinstance(value, (bytes, bytearray)):
processed_row[key] = f"0x{value.hex()}"
else:
processed_row[key] = value
rows.append(processed_row)
return rows
finally:
sess.close()

try:
if use_database_executor:
result = await get_instance(call.hass).async_add_executor_job(
_execute_and_convert_query
)
else:
result = await call.hass.async_add_executor_job(_execute_and_convert_query)
except SQLAlchemyError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="query_execution_error",
translation_placeholders={"error": redact_credentials(str(err))},
) from err

return {"result": result}


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the SQL integration."""

hass.services.async_register(
DOMAIN,
SERVICE_QUERY,
_async_query_service,
schema=SERVICE_QUERY_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
28 changes: 28 additions & 0 deletions homeassistant/components/sql/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Describes the format for services provided by the SQL integration.

query:
fields:
query:
required: true
example: |
SELECT
states.state,
last_updated_ts
FROM
states
INNER JOIN states_meta ON
states.metadata_id = states_meta.metadata_id
WHERE
states_meta.entity_id = 'sun.sun'
ORDER BY
last_updated_ts DESC
LIMIT
10;
selector:
text:
multiline: true
db_url:
required: false
example: "sqlite:////config/home-assistant_v2.db"
selector:
text:
27 changes: 27 additions & 0 deletions homeassistant/components/sql/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,33 @@
}
}
},
"exceptions": {
"db_connection_failed": {
"message": "Failed to connect to the database: {db_url}"
},
"query_execution_error": {
"message": "An error occurred when executing the query: {error}"
},
"query_not_allowed": {
"message": "The provided query is not allowed: {error}"
}
},
"services": {
"query": {
"name": "Query",
"description": "Executes a SQL query and returns the result.",
"fields": {
"query": {
"name": "Query",
"description": "The SELECT query to execute."
},
"db_url": {
"name": "Database URL",
"description": "The URL of the database to connect to. If not provided, the default Home Assistant recorder database will be used."
}
}
}
},
"options": {
"step": {
"init": {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/starline/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
SensorEntityDescription(
key="fuel",
translation_key="fuel",
device_class=SensorDeviceClass.VOLUME,
# No device_class: fuel can be reported as percentage or volume depending on vehicle
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
Expand Down
Loading
Loading