1
0
mirror of https://github.com/home-assistant/core.git synced 2025-08-25 21:30:05 +00:00
Files
.devcontainer
.github
.vscode
homeassistant
auth
backports
brands
components
3_day_blinds
abode
accuweather
acer_projector
acmeda
acomax
actiontec
adax
adguard
ads
advantage_air
aemet
aep_ohio
aep_texas
aftership
agent_dvr
air_quality
airgradient
airly
airnow
airq
airthings
airthings_ble
airtouch4
airtouch5
airvisual
airvisual_pro
airzone
airzone_cloud
aladdin_connect
alarm_control_panel
alarmdecoder
alert
alexa
alpha_vantage
amazon_polly
amberelectric
ambient_network
ambient_station
amcrest
amp_motorization
ampio
analytics
analytics_insights
android_ip_webcam
androidtv
androidtv_remote
anel_pwrctrl
anova
anthemav
anwb_energie
aosmith
apache_kafka
apcupsd
api
appalachianpower
apple_tv
application_credentials
apprise
aprilaire
aprs
apsystems
aqualogic
aquostv
aranet
arcam_fmj
arest
arris_tg2492lg
aruba
arve
arwn
aseko_pool_live
assist_pipeline
asterisk_cdr
asterisk_mbox
asuswrt
atag
aten_pe
atlanticcityelectric
atome
august
august_ble
aurora
aurora_abb_powerone
aussie_broadband
auth
automation
avea
avion
awair
aws
axis
azure_data_explorer
azure_devops
azure_event_hub
azure_service_bus
backup
baf
baidu
balboa
bang_olufsen
bayesian
bbox
beewi_smartclim
bge
binary_sensor
bitcoin
bizkaibus
blackbird
blebox
blink
blinksticklight
bliss_automation
bloc_blinds
blockchain
bloomsky
blue_current
bluemaestro
blueprint
bluesound
bluetooth
bluetooth_adapters
bluetooth_le_tracker
bluetooth_tracker
bmw_connected_drive
bond
bosch_shc
brandt
braviatv
brel_home
bring
broadlink
brother
brottsplatskartan
browser
brunt
bsblan
bswitch
bt_home_hub_5
bt_smarthub
bthome
bticino
bubendorff
buienradar
button
caldav
calendar
camera
canary
cast
ccm15
cert_expiry
channels
circuit
cisco_ios
cisco_mobility_express
cisco_webex_teams
citybikes
clementine
clickatell
clicksend
clicksend_tts
climate
cloud
cloudflare
cmus
co2signal
coautilities
coinbase
color_extractor
comed
comed_hourly_pricing
comelit
comfoconnect
command_line
compensation
concord232
coned
config
configurator
control4
conversation
coolmaster
counter
cover
cozytouch
cppm_tracker
cpuspeed
cribl
crownstone
cups
currencylayer
dacia
daikin
danfoss_air
datadog
date
datetime
ddwrt
debugpy
deconz
decora
decora_wifi
default_config
delijn
delmarva
deluge
demo
denon
denonavr
derivative
devialet
device_automation
device_sun_light_trigger
device_tracker
devolo_home_control
devolo_home_network
dexcom
dhcp
diagnostics
dialogflow
diaz
digital_loggers
digital_ocean
directv
discogs
discord
discovergy
dlib_face_detect
dlib_face_identify
dlink
dlna_dmr
dlna_dms
dnsip
dominos
doods
doorbird
dooya
dormakaba_dkey
dovado
downloader
dremel_3d_printer
drop_connect
dsmr
dsmr_reader
dte_energy_bridge
dublin_bus_transport
duckdns
dunehd
duotecno
duquesne_light
dwd_weather_warnings
dweet
dynalite
eafm
eastron
easyenergy
ebox
ebusd
ecoal_boiler
ecobee
ecoforest
econet
ecovacs
ecowitt
eddystone_temperature
edimax
edl21
efergy
egardia
eight_sleep
electrasmart
electric_kiwi
elgato
eliqonline
elkm1
elmax
elv
elvia
emby
emoncms
emoncms_history
emonitor
emulated_hue
emulated_kasa
emulated_roku
energenie_power_sockets
energie_vanons
energy
energyzero
enigma2
enmax
enocean
enphase_envoy
entur_public_transport
environment_canada
envisalink
ephember
epic_games_store
epion
epson
eq3btsmart
escea
esera_onewire
esphome
etherscan
eufy
eufylife_ble
event
evergy
everlights
evil_genius_labs
evohome
ezviz
faa_delays
facebook
fail2ban
familyhub
fan
fastdotcom
feedreader
ffmpeg
ffmpeg_motion
ffmpeg_noise
fibaro
fido
file
file_upload
filesize
filter
fints
fire_tv
fireservicerota
firmata
fitbit
fivem
fixer
fjaraskupan
fleetgo
flexit
flexit_bacnet
flexom
flic
flick_electric
flipr
flo
flock
flume
flux
flux_led
folder
folder_watcher
foobot
forecast_solar
forked_daapd
fortios
foscam
foursquare
free_mobile
freebox
freedns
freedompro
fritz
fritzbox
fritzbox_callmonitor
fronius
frontend
frontier_silicon
fujitsu_anywair
fully_kiosk
futurenow
fyta
garadget
garages_amsterdam
gardena_bluetooth
gaviota
gc100
gdacs
generic
generic_hygrostat
generic_thermostat
geniushub
geo_json_events
geo_location
geo_rss_events
geocaching
geofency
geonetnz_quakes
geonetnz_volcano
gios
github
gitlab_ci
gitter
glances
goalzero
gogogate2
goodwe
google
google_assistant
google_assistant_sdk
google_cloud
google_domains
google_generative_ai_conversation
google_mail
google_maps
google_pubsub
google_sheets
google_tasks
google_translate
google_travel_time
google_wifi
govee_ble
govee_light_local
gpsd
gpslogger
graphite
gree
greeneye_monitor
greenwave
group
growatt_server
gstreamer
gtfs
guardian
habitica
hardkernel
hardware
harman_kardon_avr
harmony
hassio
havana_shade
haveibeenpwned
hddtemp
hdmi_cec
heatmiser
heiwa
heos
here_travel_time
hexaom
hi_kumo
hikvision
hikvisioncam
hisense_aehw4a1
history
history_stats
hitron_coda
hive
hko
hlk_sw16
holiday
home_connect
home_plus_control
homeassistant
homeassistant_alerts
homeassistant_green
homeassistant_hardware
homeassistant_sky_connect
homeassistant_yellow
homekit
homekit_controller
homematic
homematicip_cloud
homewizard
homeworks
__init__.py
binary_sensor.py
button.py
config_flow.py
const.py
icons.json
light.py
manifest.json
services.yaml
strings.json
honeywell
horizon
hp_ilo
html5
http
huawei_lte
hue
huisbaasje
humidifier
hunterdouglas_powerview
hurrican_shutters_wholesale
husqvarna_automower
huum
hvv_departures
hydrawise
hyperion
ialarm
iammeter
iaqualink
ibeacon
icloud
idasen_desk
idteck_prox
ifttt
iglo
ign_sismologia
ihc
image
image_processing
image_upload
imap
imgw_pib
improv_ble
incomfort
indianamichiganpower
influxdb
inkbird
input_boolean
input_button
input_datetime
input_number
input_select
input_text
inspired_shades
insteon
integration
intellifire
intent
intent_script
intesishome
ios
iotawatt
iperf3
ipma
ipp
iqvia
irish_rail_transport
isal
islamic_prayer_times
ismartwindow
iss
isy994
itach
itunes
izone
jellyfin
jewish_calendar
joaoapps_join
juicenet
justnimbus
jvc_projector
kaiterra
kaleidescape
kankun
keba
keenetic_ndms2
kef
kegtron
kentuckypower
keyboard
keyboard_remote
keymitt_ble
kira
kitchen_sink
kiwi
kmtronic
knx
kodi
konnected
kostal_plenticore
kraken
krispol
kulersky
kwb
lacrosse
lacrosse_view
lamarzocco
lametric
landisgyr_heat_meter
lannouncer
lastfm
launch_library
laundrify
lawn_mower
lcn
ld2410_ble
leaone
led_ble
legrand
lg_netcast
lg_soundbar
lidarr
life360
lifx
lifx_cloud
light
lightwave
limitlessled
linear_garage_door
linksys_smart
linode
linux_battery
lirc
litejet
litterrobot
livisi
llamalab_automate
local_calendar
local_file
local_ip
local_todo
locative
lock
logbook
logentries
logger
logi_circle
london_air
london_underground
lookin
loqed
lovelace
luci
luftdaten
lupusec
lutron
lutron_caseta
luxaflex
lw12wifi
lyric
madeco
mailbox
mailgun
manual
manual_mqtt
map
marantz
martec
marytts
mastodon
matrix
matter
maxcube
mazda
meater
medcom_ble
media_extractor
media_player
media_source
mediaroom
melcloud
melissa
melnor
meraki
message_bird
met
met_eireann
meteo_france
meteoalarm
meteoclimatic
metoffice
mfi
microbees
microsoft
microsoft_face
microsoft_face_detect
microsoft_face_identify
mijndomein_energie
mikrotik
mill
min_max
minecraft_server
minio
mjpeg
moat
mobile_app
mochad
modbus
modem_callerid
modern_forms
moehlenhoff_alpha2
mold_indicator
monessen
monoprice
monzo
moon
mopeka
motion_blinds
motionblinds_ble
motioneye
motionmount
mpd
mqtt
mqtt_eventstream
mqtt_json
mqtt_room
mqtt_statestream
msteams
mullvad
mutesync
mvglive
my
mycroft
myq
mysensors
mystrom
mythicbeastsdns
myuplink
nad
nam
namecheapdns
nanoleaf
neato
nederlandse_spoorwegen
ness_alarm
nest
netatmo
netdata
netgear
netgear_lte
netio
network
neurio_energy
nexia
nexity
nextbus
nextcloud
nextdns
nfandroidtv
nibe_heatpump
nightscout
niko_home_control
nilu
nina
nissan_leaf
nmap_tracker
nmbs
no_ip
noaa_tides
nobo_hub
norway_air
notify
notify_events
notion
nsw_fuel_station
nsw_rural_fire_service_feed
nuheat
nuki
numato
number
nut
nutrichef
nws
nx584
nzbget
oasa_telematics
obihai
octoprint
oem
ohmconnect
ollama
ombi
omnilogic
onboarding
oncue
ondilo_ico
onewire
onkyo
onvif
open_meteo
openai_conversation
openalpr_cloud
openerz
openevse
openexchangerates
opengarage
openhardwaremonitor
openhome
opensensemap
opensky
opentherm_gw
openuv
openweathermap
opnsense
opower
opple
oralb
oru
oru_opower
orvibo
osoenergy
osramlightify
otbr
otp
ourgroceries
overkiz
ovo_energy
owntracks
p1_monitor
panasonic_bluray
panasonic_viera
pandora
panel_custom
panel_iframe
pcs_lighting
peco
peco_opower
pegel_online
pencom
pepco
permobil
persistent_notification
person
pge
philips_js
pi_hole
picnic
picotts
pilight
ping
pioneer
piper
pjlink
plaato
plant
plex
plugwise
plum_lightpad
pocketcasts
point
poolsense
portlandgeneral
powerwall
private_ble_device
profiler
progettihwsw
proliphix
prometheus
prosegur
prowl
proximity
proxmoxve
proxy
prusalink
ps4
pse
psoklahoma
pulseaudio_loopback
pure_energie
purpleair
push
pushbullet
pushover
pushsafer
pvoutput
pvpc_hourly_pricing
pyload
python_script
qbittorrent
qingping
qld_bushfire
qnap
qnap_qsw
qrcode
quadrafire
quantum_gateway
qvr_pro
qwikswitch
rabbitair
rachio
radarr
radio_browser
radiotherm
rainbird
raincloud
rainforest_eagle
rainforest_raven
rainmachine
random
rapt_ble
raspberry_pi
raspyrfm
raven_rock_mfg
rdw
recollect_waste
recorder
recovery_mode
recswitch
reddit
refoss
rejseplanen
remember_the_milk
remote
remote_rpi_gpio
renault
renson
reolink
repairs
repetier
rest
rest_command
rexel
rflink
rfxtrx
rhasspy
ridwell
ring
ripple
risco
rituals_perfume_genie
rmvtransport
roborock
rocketchat
roku
romy
roomba
roon
route53
rova
rpi_camera
rpi_power
rss_feed_template
rtorrent
rtsp_to_webrtc
ruckus_unleashed
russound_rio
russound_rnet
ruuvi_gateway
ruuvitag_ble
rympro
sabnzbd
saj
samsam
samsungtv
sanix
satel_integra
scene
schedule
schlage
schluter
scl
scrape
screenaway
screenlogic
script
scsgate
search
season
select
sendgrid
sense
sensibo
sensirion_ble
sensor
sensorblue
sensorpro
sensorpush
sentry
senz
serial
serial_pm
sesame
seven_segments
seventeentrack
sfr_box
sharkiq
shell_command
shelly
shodan
shopping_list
sia
sigfox
sighthound
signal_messenger
simplepush
simplisafe
simply_automated
simu
simulated
sinch
siren
sisyphus
sky_hub
skybeacon
skybell
slack
sleepiq
slide
slimproto
sma
smappee
smart_blinds
smart_home
smart_meter_texas
smarther
smartthings
smarttub
smarty
smhi
sms
smtp
smud
snapcast
snips
snmp
snooz
solaredge
solaredge_local
solarlog
solax
soma
somfy
somfy_mylink
sonarr
songpal
sonos
sony_projector
soundtouch
spaceapi
spc
speedtestdotnet
spider
splunk
spotify
sql
squeezebox
srp_energy
ssdp
starline
starlingbank
starlink
startca
statistics
statsd
steam_online
steamist
stiebel_eltron
stookalert
stookwijzer
stream
streamlabswater
stt
subaru
suez_water
sun
sunweg
supervisord
supla
surepetcare
swepco
swiss_hydrological_data
swiss_public_transport
swisscom
switch
switch_as_x
switchbee
switchbot
switchbot_cloud
switcher_kis
switchmate
symfonisk
syncthing
syncthru
synology_chat
synology_dsm
synology_srm
syslog
system_bridge
system_health
system_log
systemmonitor
tado
tag
tailscale
tailwind
tami4
tank_utility
tankerkoenig
tapsaff
tasmota
tautulli
tcp
technove
ted5000
tedee
telegram
telegram_bot
tellduslive
tellstick
telnet
temper
template
tensorflow
tesla_wall_connector
teslemetry
tessie
text
tfiac
thermobeacon
thermoplus
thermopro
thermoworks_smoke
thethingsnetwork
thingspeak
thinkingcleaner
thomson
thread
threshold
tibber
tikteck
tile
tilt_ble
time
time_date
timer
tmb
tod
todo
todoist
tolo
tomato
tomorrowio
toon
torque
totalconnect
touchline
tplink
tplink_lte
tplink_omada
tplink_tapo
traccar
traccar_server
trace
tractive
tradfri
trafikverket_camera
trafikverket_ferry
trafikverket_train
trafikverket_weatherstation
transmission
transport_nsw
travisci
trend
tts
tuya
twentemilieu
twilio
twilio_call
twilio_sms
twinkly
twitch
twitter
ubiwizz
ubus
ue_smart_radio
uk_transport
ukraine_alarm
ultraloq
unifi
unifi_direct
unifiled
unifiprotect
universal
upb
upc_connect
upcloud
update
upnp
uprise_smart_shades
uptime
uptimerobot
usb
usgs_earthquakes_feed
utility_meter
uvc
v2c
vacuum
vallox
valve
vasttrafik
velbus
velux
venstar
vera
verisure
vermont_castings
versasense
version
vesync
viaggiatreno
vicare
vilfo
vivotek
vizio
vlc
vlc_telnet
vodafone_station
voicerss
voip
volkszaehler
volumio
volvooncall
vulcan
vultr
w800rf32
wake_on_lan
wake_word
wallbox
waqi
water_heater
waterfurnace
watson_iot
watson_tts
watttime
waze_travel_time
weather
weatherflow
weatherflow_cloud
weatherkit
webhook
webmin
webostv
websocket_api
wemo
whirlpool
whisper
whois
wiffi
wilight
wirelesstag
withings
wiz
wled
wolflink
workday
worldclock
worldtidesinfo
worxlandroid
ws66i
wsdot
wyoming
x10
xbox
xeoma
xiaomi
xiaomi_aqara
xiaomi_ble
xiaomi_miio
xiaomi_tv
xmpp
xs1
yale_home
yale_smart_alarm
yalexs_ble
yamaha
yamaha_musiccast
yandex_transport
yandextts
yardian
yeelight
yeelightsunflower
yi
yolink
youless
youtube
zabbix
zamg
zengge
zeroconf
zerproc
zestimate
zeversolar
zha
zhong_hong
ziggo_mediabox_xl
zodiac
zondergas
zone
zoneminder
zwave_js
zwave_me
__init__.py
generated
helpers
scripts
util
__init__.py
__main__.py
block_async_io.py
bootstrap.py
config.py
config_entries.py
const.py
core.py
data_entry_flow.py
exceptions.py
loader.py
package_constraints.txt
py.typed
requirements.py
runner.py
setup.py
strings.json
machine
pylint
rootfs
script
tests
.core_files.yaml
.coveragerc
.dockerignore
.git-blame-ignore-revs
.gitattributes
.gitignore
.hadolint.yaml
.pre-commit-config.yaml
.prettierignore
.strict-typing
.yamllint
CLA.md
CODEOWNERS
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
Dockerfile.dev
LICENSE.md
MANIFEST.in
README.rst
build.yaml
codecov.yml
mypy.ini
pyproject.toml
requirements.txt
requirements_all.txt
requirements_test.txt
requirements_test_all.txt
requirements_test_pre_commit.txt
core/homeassistant/components/homeworks/config_flow.py

736 lines
24 KiB
Python

"""Lutron Homeworks Series 4 and 8 config flow."""
from __future__ import annotations
from functools import partial
import logging
from typing import Any
from pyhomeworks.pyhomeworks import Homeworks
import voluptuous as vol
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
async_get_hass,
callback,
)
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
issue_registry as ir,
selector,
)
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaFlowError,
SchemaFlowFormStep,
SchemaFlowMenuStep,
SchemaOptionsFlowHandler,
)
from homeassistant.helpers.selector import TextSelector
from homeassistant.util import slugify
from . import DEFAULT_FADE_RATE, calculate_unique_id
from .const import (
CONF_ADDR,
CONF_BUTTONS,
CONF_CONTROLLER_ID,
CONF_DIMMERS,
CONF_INDEX,
CONF_KEYPADS,
CONF_LED,
CONF_NUMBER,
CONF_RATE,
CONF_RELEASE_DELAY,
DEFAULT_BUTTON_NAME,
DEFAULT_KEYPAD_NAME,
DEFAULT_LIGHT_NAME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
CONTROLLER_EDIT = {
vol.Required(CONF_HOST): selector.TextSelector(),
vol.Required(CONF_PORT): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1,
max=65535,
mode=selector.NumberSelectorMode.BOX,
)
),
}
LIGHT_EDIT = {
vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=20,
mode=selector.NumberSelectorMode.SLIDER,
step=0.1,
)
),
}
BUTTON_EDIT = {
vol.Optional(CONF_LED, default=False): selector.BooleanSelector(),
vol.Optional(CONF_RELEASE_DELAY, default=0): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=5,
step=0.01,
mode=selector.NumberSelectorMode.BOX,
unit_of_measurement="s",
),
),
}
validate_addr = cv.matches_regex(r"\[(?:\d\d:)?\d\d:\d\d:\d\d\]")
async def validate_add_controller(
handler: ConfigFlow | SchemaOptionsFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate controller setup."""
user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME])
user_input[CONF_PORT] = int(user_input[CONF_PORT])
try:
handler._async_abort_entries_match( # noqa: SLF001
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
except AbortFlow as err:
raise SchemaFlowError("duplicated_host_port") from err
try:
handler._async_abort_entries_match( # noqa: SLF001
{CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]}
)
except AbortFlow as err:
raise SchemaFlowError("duplicated_controller_id") from err
await _try_connection(user_input)
return user_input
async def _try_connection(user_input: dict[str, Any]) -> None:
"""Try connecting to the controller."""
def _try_connect(host: str, port: int) -> None:
"""Try connecting to the controller.
Raises ConnectionError if the connection fails.
"""
_LOGGER.debug(
"Trying to connect to %s:%s", user_input[CONF_HOST], user_input[CONF_PORT]
)
controller = Homeworks(host, port, lambda msg_types, values: None)
controller.close()
controller.join()
hass = async_get_hass()
try:
await hass.async_add_executor_job(
_try_connect, user_input[CONF_HOST], user_input[CONF_PORT]
)
except ConnectionError as err:
raise SchemaFlowError("connection_error") from err
except Exception as err:
_LOGGER.exception("Caught unexpected exception")
raise SchemaFlowError("unknown_error") from err
def _create_import_issue(hass: HomeAssistant) -> None:
"""Create a repair issue asking the user to remove YAML."""
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.6.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Lutron Homeworks",
},
)
def _validate_address(handler: SchemaCommonFlowHandler, addr: str) -> None:
"""Validate address."""
try:
validate_addr(addr)
except vol.Invalid as err:
raise SchemaFlowError("invalid_addr") from err
for _key in (CONF_DIMMERS, CONF_KEYPADS):
items: list[dict[str, Any]] = handler.options[_key]
for item in items:
if item[CONF_ADDR] == addr:
raise SchemaFlowError("duplicated_addr")
def _validate_button_number(handler: SchemaCommonFlowHandler, number: int) -> None:
"""Validate button number."""
keypad = handler.flow_state["_idx"]
buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS]
for button in buttons:
if button[CONF_NUMBER] == number:
raise SchemaFlowError("duplicated_number")
async def validate_add_button(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate button input."""
user_input[CONF_NUMBER] = int(user_input[CONF_NUMBER])
_validate_button_number(handler, user_input[CONF_NUMBER])
# Standard behavior is to merge the result with the options.
# In this case, we want to add a sub-item so we update the options directly.
keypad = handler.flow_state["_idx"]
buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS]
buttons.append(user_input)
return {}
async def validate_add_keypad(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate keypad or light input."""
_validate_address(handler, user_input[CONF_ADDR])
# Standard behavior is to merge the result with the options.
# In this case, we want to add a sub-item so we update the options directly.
items = handler.options[CONF_KEYPADS]
items.append(user_input | {CONF_BUTTONS: []})
return {}
async def validate_add_light(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate light input."""
_validate_address(handler, user_input[CONF_ADDR])
# Standard behavior is to merge the result with the options.
# In this case, we want to add a sub-item so we update the options directly.
items = handler.options[CONF_DIMMERS]
items.append(user_input)
return {}
async def get_select_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for selecting a button."""
keypad = handler.flow_state["_idx"]
buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS]
return vol.Schema(
{
vol.Required(CONF_INDEX): vol.In(
{
str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})"
for index, config in enumerate(buttons)
},
)
}
)
async def get_select_keypad_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for selecting a keypad."""
return vol.Schema(
{
vol.Required(CONF_INDEX): vol.In(
{
str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})"
for index, config in enumerate(handler.options[CONF_KEYPADS])
},
)
}
)
async def get_select_light_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for selecting a light."""
return vol.Schema(
{
vol.Required(CONF_INDEX): vol.In(
{
str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})"
for index, config in enumerate(handler.options[CONF_DIMMERS])
},
)
}
)
async def validate_select_button(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Store button index in flow state."""
handler.flow_state["_button_idx"] = int(user_input[CONF_INDEX])
return {}
async def validate_select_keypad_light(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Store keypad or light index in flow state."""
handler.flow_state["_idx"] = int(user_input[CONF_INDEX])
return {}
async def get_edit_button_suggested_values(
handler: SchemaCommonFlowHandler,
) -> dict[str, Any]:
"""Return suggested values for button editing."""
keypad_idx: int = handler.flow_state["_idx"]
button_idx: int = handler.flow_state["_button_idx"]
return dict(handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS][button_idx])
async def get_edit_light_suggested_values(
handler: SchemaCommonFlowHandler,
) -> dict[str, Any]:
"""Return suggested values for light editing."""
idx: int = handler.flow_state["_idx"]
return dict(handler.options[CONF_DIMMERS][idx])
async def validate_button_edit(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Update edited keypad or light."""
# Standard behavior is to merge the result with the options.
# In this case, we want to add a sub-item so we update the options directly.
keypad_idx: int = handler.flow_state["_idx"]
button_idx: int = handler.flow_state["_button_idx"]
buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS]
buttons[button_idx].update(user_input)
return {}
async def validate_light_edit(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Update edited keypad or light."""
# Standard behavior is to merge the result with the options.
# In this case, we want to add a sub-item so we update the options directly.
idx: int = handler.flow_state["_idx"]
handler.options[CONF_DIMMERS][idx].update(user_input)
return {}
async def get_remove_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for button removal."""
keypad_idx: int = handler.flow_state["_idx"]
buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS]
return vol.Schema(
{
vol.Required(CONF_INDEX): cv.multi_select(
{
str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})"
for index, config in enumerate(buttons)
},
)
}
)
async def get_remove_keypad_light_schema(
handler: SchemaCommonFlowHandler, *, key: str
) -> vol.Schema:
"""Return schema for keypad or light removal."""
return vol.Schema(
{
vol.Required(CONF_INDEX): cv.multi_select(
{
str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})"
for index, config in enumerate(handler.options[key])
},
)
}
)
async def validate_remove_button(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate remove keypad or light."""
removed_indexes: set[str] = set(user_input[CONF_INDEX])
# Standard behavior is to merge the result with the options.
# In this case, we want to remove sub-items so we update the options directly.
entity_registry = er.async_get(handler.parent_handler.hass)
keypad_idx: int = handler.flow_state["_idx"]
keypad: dict = handler.options[CONF_KEYPADS][keypad_idx]
items: list[dict[str, Any]] = []
item: dict[str, Any]
for index, item in enumerate(keypad[CONF_BUTTONS]):
if str(index) not in removed_indexes:
items.append(item)
button_number = keypad[CONF_BUTTONS][index][CONF_NUMBER]
for domain in (BINARY_SENSOR_DOMAIN, BUTTON_DOMAIN):
if entity_id := entity_registry.async_get_entity_id(
domain,
DOMAIN,
calculate_unique_id(
handler.options[CONF_CONTROLLER_ID],
keypad[CONF_ADDR],
button_number,
),
):
entity_registry.async_remove(entity_id)
keypad[CONF_BUTTONS] = items
return {}
async def validate_remove_keypad_light(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any], *, key: str
) -> dict[str, Any]:
"""Validate remove keypad or light."""
removed_indexes: set[str] = set(user_input[CONF_INDEX])
# Standard behavior is to merge the result with the options.
# In this case, we want to remove sub-items so we update the options directly.
entity_registry = er.async_get(handler.parent_handler.hass)
items: list[dict[str, Any]] = []
item: dict[str, Any]
for index, item in enumerate(handler.options[key]):
if str(index) not in removed_indexes:
items.append(item)
elif key != CONF_DIMMERS:
continue
if entity_id := entity_registry.async_get_entity_id(
LIGHT_DOMAIN,
DOMAIN,
calculate_unique_id(
handler.options[CONF_CONTROLLER_ID], item[CONF_ADDR], 0
),
):
entity_registry.async_remove(entity_id)
handler.options[key] = items
return {}
DATA_SCHEMA_ADD_CONTROLLER = vol.Schema(
{
vol.Required(
CONF_NAME, description={"suggested_value": "Lutron Homeworks"}
): selector.TextSelector(),
**CONTROLLER_EDIT,
}
)
DATA_SCHEMA_EDIT_CONTROLLER = vol.Schema(CONTROLLER_EDIT)
DATA_SCHEMA_ADD_LIGHT = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_LIGHT_NAME): TextSelector(),
vol.Required(CONF_ADDR): TextSelector(),
**LIGHT_EDIT,
}
)
DATA_SCHEMA_ADD_KEYPAD = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_KEYPAD_NAME): TextSelector(),
vol.Required(CONF_ADDR): TextSelector(),
}
)
DATA_SCHEMA_ADD_BUTTON = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_BUTTON_NAME): TextSelector(),
vol.Required(CONF_NUMBER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1,
max=24,
step=1,
mode=selector.NumberSelectorMode.BOX,
),
),
**BUTTON_EDIT,
}
)
DATA_SCHEMA_EDIT_BUTTON = vol.Schema(BUTTON_EDIT)
DATA_SCHEMA_EDIT_LIGHT = vol.Schema(LIGHT_EDIT)
OPTIONS_FLOW = {
"init": SchemaFlowMenuStep(
[
"add_keypad",
"select_edit_keypad",
"remove_keypad",
"add_light",
"select_edit_light",
"remove_light",
]
),
"add_keypad": SchemaFlowFormStep(
DATA_SCHEMA_ADD_KEYPAD,
suggested_values=None,
validate_user_input=validate_add_keypad,
),
"select_edit_keypad": SchemaFlowFormStep(
get_select_keypad_schema,
suggested_values=None,
validate_user_input=validate_select_keypad_light,
next_step="edit_keypad",
),
"edit_keypad": SchemaFlowMenuStep(
[
"add_button",
"select_edit_button",
"remove_button",
]
),
"add_button": SchemaFlowFormStep(
DATA_SCHEMA_ADD_BUTTON,
suggested_values=None,
validate_user_input=validate_add_button,
),
"select_edit_button": SchemaFlowFormStep(
get_select_button_schema,
suggested_values=None,
validate_user_input=validate_select_button,
next_step="edit_button",
),
"edit_button": SchemaFlowFormStep(
DATA_SCHEMA_EDIT_BUTTON,
suggested_values=get_edit_button_suggested_values,
validate_user_input=validate_button_edit,
),
"remove_button": SchemaFlowFormStep(
get_remove_button_schema,
suggested_values=None,
validate_user_input=validate_remove_button,
),
"remove_keypad": SchemaFlowFormStep(
partial(get_remove_keypad_light_schema, key=CONF_KEYPADS),
suggested_values=None,
validate_user_input=partial(validate_remove_keypad_light, key=CONF_KEYPADS),
),
"add_light": SchemaFlowFormStep(
DATA_SCHEMA_ADD_LIGHT,
suggested_values=None,
validate_user_input=validate_add_light,
),
"select_edit_light": SchemaFlowFormStep(
get_select_light_schema,
suggested_values=None,
validate_user_input=validate_select_keypad_light,
next_step="edit_light",
),
"edit_light": SchemaFlowFormStep(
DATA_SCHEMA_EDIT_LIGHT,
suggested_values=get_edit_light_suggested_values,
validate_user_input=validate_light_edit,
),
"remove_light": SchemaFlowFormStep(
partial(get_remove_keypad_light_schema, key=CONF_DIMMERS),
suggested_values=None,
validate_user_input=partial(validate_remove_keypad_light, key=CONF_DIMMERS),
),
}
class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Lutron Homeworks."""
import_config: dict[str, Any]
async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult:
"""Start importing configuration from yaml."""
self.import_config = {
CONF_HOST: config[CONF_HOST],
CONF_PORT: config[CONF_PORT],
CONF_DIMMERS: [
{
CONF_ADDR: light[CONF_ADDR],
CONF_NAME: light[CONF_NAME],
CONF_RATE: light[CONF_RATE],
}
for light in config[CONF_DIMMERS]
],
CONF_KEYPADS: [
{
CONF_ADDR: keypad[CONF_ADDR],
CONF_BUTTONS: [],
CONF_NAME: keypad[CONF_NAME],
}
for keypad in config[CONF_KEYPADS]
],
}
return await self.async_step_import_controller_name()
async def async_step_import_controller_name(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Ask user to set a name of the controller."""
errors = {}
try:
self._async_abort_entries_match(
{
CONF_HOST: self.import_config[CONF_HOST],
CONF_PORT: self.import_config[CONF_PORT],
}
)
except AbortFlow:
_create_import_issue(self.hass)
raise
if user_input:
try:
user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME])
self._async_abort_entries_match(
{CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]}
)
except AbortFlow:
errors["base"] = "duplicated_controller_id"
else:
self.import_config |= user_input
return await self.async_step_import_finish()
return self.async_show_form(
step_id="import_controller_name",
data_schema=vol.Schema(
{
vol.Required(
CONF_NAME, description={"suggested_value": "Lutron Homeworks"}
): selector.TextSelector(),
}
),
errors=errors,
)
async def async_step_import_finish(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Ask user to remove YAML configuration."""
if user_input is not None:
entity_registry = er.async_get(self.hass)
config = self.import_config
for light in config[CONF_DIMMERS]:
addr = light[CONF_ADDR]
if entity_id := entity_registry.async_get_entity_id(
LIGHT_DOMAIN, DOMAIN, f"homeworks.{addr}"
):
entity_registry.async_update_entity(
entity_id,
new_unique_id=calculate_unique_id(
config[CONF_CONTROLLER_ID], addr, 0
),
)
name = config.pop(CONF_NAME)
return self.async_create_entry(
title=name,
data={},
options=config,
)
return self.async_show_form(step_id="import_finish", data_schema=vol.Schema({}))
async def _validate_edit_controller(
self, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate controller setup."""
user_input[CONF_PORT] = int(user_input[CONF_PORT])
our_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert our_entry
other_entries = self._async_current_entries()
for entry in other_entries:
if entry.entry_id == our_entry.entry_id:
continue
if (
user_input[CONF_HOST] == entry.options[CONF_HOST]
and user_input[CONF_PORT] == entry.options[CONF_PORT]
):
raise SchemaFlowError("duplicated_host_port")
await _try_connection(user_input)
return user_input
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfigure flow."""
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry
errors = {}
suggested_values = {
CONF_HOST: entry.options[CONF_HOST],
CONF_PORT: entry.options[CONF_PORT],
}
if user_input:
suggested_values = {
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
try:
await self._validate_edit_controller(user_input)
except SchemaFlowError as err:
errors["base"] = str(err)
else:
new_options = entry.options | {
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
return self.async_update_reload_and_abort(
entry,
options=new_options,
reason="reconfigure_successful",
reload_even_if_entry_is_unchanged=False,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
DATA_SCHEMA_EDIT_CONTROLLER, suggested_values
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
if user_input:
try:
await validate_add_controller(self, user_input)
except SchemaFlowError as err:
errors["base"] = str(err)
else:
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
name = user_input.pop(CONF_NAME)
user_input |= {CONF_DIMMERS: [], CONF_KEYPADS: []}
return self.async_create_entry(title=name, data={}, options=user_input)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA_ADD_CONTROLLER,
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
"""Options flow handler for Lutron Homeworks."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)