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: 2 additions & 0 deletions CODEOWNERS

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

20 changes: 20 additions & 0 deletions homeassistant/components/novy_cooker_hood/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""The Novy Cooker Hood integration."""

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

PLATFORMS: list[Platform] = [Platform.LIGHT]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Novy Cooker Hood from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
127 changes: 127 additions & 0 deletions homeassistant/components/novy_cooker_hood/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Config flow for the Novy Cooker Hood integration."""

from __future__ import annotations

import asyncio
from typing import Any

import voluptuous as vol

from homeassistant.components.radio_frequency import (
async_get_transmitters,
async_send_command,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector

from .const import CODE_MAX, CODE_MIN, CONF_CODE, CONF_TRANSMITTER, DEFAULT_CODE, DOMAIN
from .light import COMMAND_LIGHT, FREQUENCY, MODULATION, get_codes_for_code

_CODE_OPTIONS = [str(code) for code in range(CODE_MIN, CODE_MAX + 1)]
_TOGGLE_GAP = 1.5


class NovyCookerHoodConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Novy Cooker Hood."""

VERSION = 1

def __init__(self) -> None:
"""Initialize the flow."""
self._transmitter_entity_id: str | None = None
self._transmitter_id: str | None = None
self._code: int = DEFAULT_CODE

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick a transmitter and code."""
try:
transmitters = async_get_transmitters(self.hass, FREQUENCY, MODULATION)
except HomeAssistantError:
return self.async_abort(reason="no_transmitters")

if not transmitters:
return self.async_abort(reason="no_compatible_transmitters")

if user_input is not None:
registry = er.async_get(self.hass)
entity_entry = registry.async_get(user_input[CONF_TRANSMITTER])
assert entity_entry is not None
code = int(user_input[CONF_CODE])
await self.async_set_unique_id(f"{entity_entry.id}_{code}")
self._abort_if_unique_id_configured()
self._transmitter_entity_id = entity_entry.entity_id
self._transmitter_id = entity_entry.id
self._code = code
return await self.async_step_test_light()

schema: dict[Any, Any] = {
vol.Required(
CONF_TRANSMITTER,
default=self._transmitter_entity_id or vol.UNDEFINED,
): selector.EntitySelector(
selector.EntitySelectorConfig(include_entities=transmitters),
),
vol.Required(CONF_CODE, default=str(self._code)): selector.SelectSelector(
selector.SelectSelectorConfig(
options=_CODE_OPTIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="code",
)
),
}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(schema),
)

async def async_step_test_light(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Toggle the hood light on then off so it ends in its starting state."""
assert self._transmitter_entity_id is not None
try:
command = await get_codes_for_code(self._code).async_load_command(
COMMAND_LIGHT
)
await async_send_command(self.hass, self._transmitter_entity_id, command)
await asyncio.sleep(_TOGGLE_GAP)
await async_send_command(self.hass, self._transmitter_entity_id, command)
except HomeAssistantError:
return await self.async_step_test_failed()
return self.async_show_menu(
step_id="test_light",
menu_options=["finish", "retry"],
description_placeholders={"code": str(self._code)},
)

async def async_step_test_failed(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Re-show the failure menu (only Retry available)."""
return self.async_show_menu(
step_id="test_failed",
menu_options=["retry"],
description_placeholders={"code": str(self._code)},
)

async def async_step_retry(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Return to the code selection step."""
return await self.async_step_user()

async def async_step_finish(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Create the config entry."""
assert self._transmitter_id is not None
return self.async_create_entry(
title="Novy Cooker Hood",
data={
CONF_TRANSMITTER: self._transmitter_id,
CONF_CODE: self._code,
},
)
14 changes: 14 additions & 0 deletions homeassistant/components/novy_cooker_hood/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Constants for the Novy Cooker Hood integration."""

from __future__ import annotations

from typing import Final

DOMAIN: Final = "novy_cooker_hood"

CONF_TRANSMITTER: Final = "transmitter"
CONF_CODE: Final = "code"

CODE_MIN: Final = 1
CODE_MAX: Final = 10
DEFAULT_CODE: Final = 1
76 changes: 76 additions & 0 deletions homeassistant/components/novy_cooker_hood/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Common entity for the Novy Cooker Hood integration."""

from __future__ import annotations

import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import Event, EventStateChangedData, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change_event

from .const import CONF_TRANSMITTER, DOMAIN

_LOGGER = logging.getLogger(__name__)


class NovyCookerHoodEntity(Entity):
"""Novy Cooker Hood base entity."""

_attr_assumed_state = True
_attr_has_entity_name = True
_attr_should_poll = False

def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the entity."""
self._transmitter = entry.data[CONF_TRANSMITTER]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Novy",
model="Cooker Hood",
)

async def async_added_to_hass(self) -> None:
"""Subscribe to transmitter entity state changes."""
await super().async_added_to_hass()

transmitter_entity_id = er.async_validate_entity_id(
er.async_get(self.hass), self._transmitter
)

@callback
def _async_transmitter_state_changed(
event: Event[EventStateChangedData],
) -> None:
"""Handle transmitter entity state changes."""
new_state = event.data["new_state"]
transmitter_available = (
new_state is not None and new_state.state != STATE_UNAVAILABLE
)
if transmitter_available != self.available:
_LOGGER.info(
"Transmitter %s used by %s is %s",
transmitter_entity_id,
self.entity_id,
"available" if transmitter_available else "unavailable",
)

self._attr_available = transmitter_available
self.async_write_ha_state()

self.async_on_remove(
async_track_state_change_event(
self.hass,
[transmitter_entity_id],
_async_transmitter_state_changed,
)
)

transmitter_state = self.hass.states.get(transmitter_entity_id)
self._attr_available = (
transmitter_state is not None
and transmitter_state.state != STATE_UNAVAILABLE
)
77 changes: 77 additions & 0 deletions homeassistant/components/novy_cooker_hood/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Light platform for the Novy Cooker Hood."""

from __future__ import annotations

from typing import Any

from rf_protocols import CodeCollection, ModulationType, get_codes

from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.components.radio_frequency import async_send_command
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity

from .const import CONF_CODE
from .entity import NovyCookerHoodEntity

PARALLEL_UPDATES = 1

FREQUENCY = 433_920_000
MODULATION = ModulationType.OOK
COMMAND_LIGHT = "light"


def get_codes_for_code(code: int) -> CodeCollection:
"""Return the bundled `rf-protocols` collection for a Novy cooker hood code."""
return get_codes(f"novy/cooker_hood/code_{code}")
Comment thread
balloob marked this conversation as resolved.


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Novy Cooker Hood light platform."""
async_add_entities([NovyCookerHoodLight(config_entry)])


class NovyCookerHoodLight(NovyCookerHoodEntity, LightEntity, RestoreEntity):
"""Novy cooker hood light toggled via a single RF press."""

_attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_translation_key = "light"

def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the light."""
super().__init__(entry)
self._codes = get_codes_for_code(entry.data[CONF_CODE])
self._attr_unique_id = entry.entry_id

async def async_added_to_hass(self) -> None:
"""Restore the last known on/off state."""
await super().async_added_to_hass()
if (last_state := await self.async_get_last_state()) is not None:
self._attr_is_on = last_state.state == STATE_ON

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on by sending the toggle command."""
await self._async_send_command(COMMAND_LIGHT)
self._attr_is_on = True
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off by sending the toggle command."""
await self._async_send_command(COMMAND_LIGHT)
self._attr_is_on = False
self.async_write_ha_state()

Comment thread
piitaya marked this conversation as resolved.
async def _async_send_command(self, name: str) -> None:
"""Load the named command and send it via the configured transmitter."""
command = await self._codes.async_load_command(name)
await async_send_command(
self.hass, self._transmitter, command, context=self._context
)
12 changes: 12 additions & 0 deletions homeassistant/components/novy_cooker_hood/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"domain": "novy_cooker_hood",
"name": "Novy Cooker Hood",
"codeowners": ["@piitaya"],
"config_flow": true,
"dependencies": ["radio_frequency"],
"documentation": "https://www.home-assistant.io/integrations/novy_cooker_hood",
"integration_type": "device",
"iot_class": "assumed_state",
"quality_scale": "bronze",
"requirements": ["rf-protocols==2.2.0"]
}
Loading
Loading