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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ homeassistant.components.pure_energie.*
homeassistant.components.purpleair.*
homeassistant.components.pushbullet.*
homeassistant.components.pvoutput.*
homeassistant.components.pyload.*
homeassistant.components.python_script.*
homeassistant.components.qbus.*
homeassistant.components.qnap_qsw.*
Expand Down
11 changes: 4 additions & 7 deletions homeassistant/components/anthropic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv

from .const import DOMAIN, LOGGER
from .const import CONF_CHAT_MODEL, DOMAIN, LOGGER, RECOMMENDED_CHAT_MODEL

PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
Expand All @@ -26,12 +26,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
partial(anthropic.AsyncAnthropic, api_key=entry.data[CONF_API_KEY])
)
try:
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
model_id = entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
model = await client.models.retrieve(model_id=model_id, timeout=10.0)
LOGGER.debug("Anthropic model: %s", model.display_name)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
Expand Down
7 changes: 1 addition & 6 deletions homeassistant/components/anthropic/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
client = await hass.async_add_executor_job(
partial(anthropic.AsyncAnthropic, api_key=data[CONF_API_KEY])
)
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
await client.models.list(timeout=10.0)


class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
Platform.TIME,
Platform.UPDATE,
Platform.VACUUM,
Platform.VALVE,
Platform.WATER_HEATER,
Platform.WEATHER,
]
Expand Down
89 changes: 89 additions & 0 deletions homeassistant/components/demo/valve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Demo valve platform that implements valves."""

from __future__ import annotations

import asyncio
from typing import Any

from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Demo config entry."""
async_add_entities(
[
DemoValve("Front Garden", ValveState.OPEN),
DemoValve("Orchard", ValveState.CLOSED),
]
)


class DemoValve(ValveEntity):
"""Representation of a Demo valve."""

_attr_should_poll = False

def __init__(
self,
name: str,
state: str,
moveable: bool = True,
) -> None:
"""Initialize the valve."""
self._attr_name = name
if moveable:
self._attr_supported_features = (
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
)
self._state = state
self._moveable = moveable

@property
def is_open(self) -> bool:
"""Return true if valve is open."""
return self._state == ValveState.OPEN

@property
def is_opening(self) -> bool:
"""Return true if valve is opening."""
return self._state == ValveState.OPENING

@property
def is_closing(self) -> bool:
"""Return true if valve is closing."""
return self._state == ValveState.CLOSING

@property
def is_closed(self) -> bool:
"""Return true if valve is closed."""
return self._state == ValveState.CLOSED

@property
def reports_position(self) -> bool:
"""Return True if entity reports position, False otherwise."""
return False

async def async_open_valve(self, **kwargs: Any) -> None:
"""Open the valve."""
self._state = ValveState.OPENING
self.async_write_ha_state()
await asyncio.sleep(OPEN_CLOSE_DELAY)
self._state = ValveState.OPEN
self.async_write_ha_state()

async def async_close_valve(self, **kwargs: Any) -> None:
"""Close the valve."""
self._state = ValveState.CLOSING
self.async_write_ha_state()
await asyncio.sleep(OPEN_CLOSE_DELAY)
self._state = ValveState.CLOSED
self.async_write_ha_state()
2 changes: 1 addition & 1 deletion homeassistant/components/ecovacs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==12.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==12.3.1"]
}
9 changes: 7 additions & 2 deletions homeassistant/components/esphome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from functools import partial
from math import isfinite
from typing import Any, cast

from aioesphomeapi import (
Expand Down Expand Up @@ -238,9 +239,13 @@ def current_temperature(self) -> float | None:
@esphome_state_property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if not self._static_info.supports_current_humidity:
if (
not self._static_info.supports_current_humidity
or (val := self._state.current_humidity) is None
or not isfinite(val)
):
return None
return round(self._state.current_humidity)
return round(val)

@property
@esphome_float_state_property
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/habitica/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
SERVICE_TRANSFORMATION = "transformation"

SERVICE_UPDATE_REWARD = "update_reward"
SERVICE_CREATE_REWARD = "create_reward"

DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/habitica/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@
"tag_options": "mdi:tag",
"developer_options": "mdi:test-tube"
}
},
"create_reward": {
"service": "mdi:treasure-chest-outline",
"sections": {
"developer_options": "mdi:test-tube"
}
}
}
}
82 changes: 55 additions & 27 deletions homeassistant/components/habitica/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_CREATE_REWARD,
SERVICE_GET_TASKS,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
Expand Down Expand Up @@ -112,18 +113,29 @@
}
)

SERVICE_UPDATE_TASK_SCHEMA = vol.Schema(
BASE_TASK_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
vol.Required(ATTR_TASK): cv.string,
vol.Optional(ATTR_RENAME): cv.string,
vol.Optional(ATTR_NOTES): cv.string,
vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_ALIAS): vol.All(
cv.string, cv.matches_regex("^[a-zA-Z0-9-_]*$")
),
vol.Optional(ATTR_COST): vol.Coerce(float),
vol.Optional(ATTR_COST): vol.All(vol.Coerce(float), vol.Range(0)),
}
)

SERVICE_UPDATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
{
vol.Required(ATTR_TASK): cv.string,
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
}
)

SERVICE_CREATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
{
vol.Required(ATTR_NAME): cv.string,
}
)

Expand Down Expand Up @@ -539,33 +551,36 @@ async def get_tasks(call: ServiceCall) -> ServiceResponse:

return result

async def update_task(call: ServiceCall) -> ServiceResponse:
"""Update task action."""
async def create_or_update_task(call: ServiceCall) -> ServiceResponse:
"""Create or update task action."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data
await coordinator.async_refresh()
is_update = call.service == SERVICE_UPDATE_REWARD
current_task = None

try:
current_task = next(
task
for task in coordinator.data.tasks
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
and task.Type is TaskType.REWARD
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="task_not_found",
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
) from e
if is_update:
try:
current_task = next(
task
for task in coordinator.data.tasks
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
and task.Type is TaskType.REWARD
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="task_not_found",
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
) from e

task_id = current_task.id
if TYPE_CHECKING:
assert task_id
data = Task()

if rename := call.data.get(ATTR_RENAME):
data["text"] = rename
if not is_update:
data["type"] = TaskType.REWARD

if (text := call.data.get(ATTR_RENAME)) or (text := call.data.get(ATTR_NAME)):
data["text"] = text

if (notes := call.data.get(ATTR_NOTES)) is not None:
data["notes"] = notes
Expand All @@ -574,7 +589,7 @@ async def update_task(call: ServiceCall) -> ServiceResponse:
remove_tags = cast(list[str], call.data.get(ATTR_REMOVE_TAG))

if tags or remove_tags:
update_tags = set(current_task.tags)
update_tags = set(current_task.tags) if current_task else set()
user_tags = {
tag.name.lower(): tag.id
for tag in coordinator.data.user.tags
Expand Down Expand Up @@ -634,7 +649,13 @@ async def create_tag(tag_name: str) -> UUID:
data["value"] = cost

try:
response = await coordinator.habitica.update_task(task_id, data)
if is_update:
if TYPE_CHECKING:
assert current_task
assert current_task.id
response = await coordinator.habitica.update_task(current_task.id, data)
else:
response = await coordinator.habitica.create_task(data)
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
Expand All @@ -659,10 +680,17 @@ async def create_tag(tag_name: str) -> UUID:
hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_REWARD,
update_task,
create_or_update_task,
schema=SERVICE_UPDATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_REWARD,
create_or_update_task,
schema=SERVICE_CREATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
Expand Down
21 changes: 17 additions & 4 deletions homeassistant/components/habitica/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,14 @@ update_reward:
rename:
selector:
text:
notes:
notes: &notes
required: false
selector:
text:
multiline: true
cost:
required: false
selector:
selector: &cost_selector
number:
min: 0
step: 0.01
Expand All @@ -163,7 +163,7 @@ update_reward:
tag_options:
collapsed: true
fields:
tag:
tag: &tag
required: false
selector:
text:
Expand All @@ -173,10 +173,23 @@ update_reward:
selector:
text:
multiple: true
developer_options:
developer_options: &developer_options
collapsed: true
fields:
alias:
required: false
selector:
text:
create_reward:
fields:
config_entry: *config_entry
name:
required: true
selector:
text:
notes: *notes
cost:
required: true
selector: *cost_selector
tag: *tag
developer_options: *developer_options
Loading
Loading