From 887feaa50a49b18e96879cbc8e0422fb24d1fa2f Mon Sep 17 00:00:00 2001 From: Franco Pellicciotti Date: Mon, 15 Jun 2026 10:53:52 -0400 Subject: [PATCH] Initil after Upgrade --- .ha_run.lock | 2 +- custom_components/powercalc/__init__.py | 24 +- .../__pycache__/__init__.cpython-314.pyc | Bin 31884 -> 31644 bytes .../__pycache__/common.cpython-314.pyc | Bin 8287 -> 8201 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 41371 -> 42191 bytes .../__pycache__/const.cpython-314.pyc | Bin 13712 -> 13907 bytes .../device_binding.cpython-314.pyc | Bin 6347 -> 6351 bytes .../__pycache__/diagnostics.cpython-314.pyc | Bin 3566 -> 3506 bytes .../__pycache__/discovery.cpython-314.pyc | Bin 36713 -> 37320 bytes .../__pycache__/errors.cpython-314.pyc | Bin 3964 -> 3964 bytes .../__pycache__/helpers.cpython-314.pyc | Bin 18793 -> 18753 bytes .../__pycache__/migrate.cpython-314.pyc | Bin 8761 -> 10884 bytes .../__pycache__/repairs.cpython-314.pyc | Bin 4706 -> 4675 bytes .../__pycache__/select.cpython-314.pyc | Bin 4954 -> 4956 bytes .../__pycache__/sensor.cpython-314.pyc | Bin 54691 -> 54751 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 156 -> 156 bytes .../__pycache__/analytics.cpython-314.pyc | Bin 17899 -> 17889 bytes .../powercalc/analytics/analytics.py | 10 +- custom_components/powercalc/common.py | 22 +- custom_components/powercalc/config_flow.py | 115 ++++--- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/global_config.cpython-314.pyc | Bin 5331 -> 5216 bytes .../powercalc/configuration/global_config.py | 6 +- custom_components/powercalc/const.py | 11 +- custom_components/powercalc/device_binding.py | 8 +- custom_components/powercalc/diagnostics.py | 4 +- custom_components/powercalc/discovery.py | 86 +++-- .../__pycache__/__init__.cpython-314.pyc | Bin 153 -> 153 bytes .../__pycache__/outlier.cpython-314.pyc | Bin 3439 -> 3439 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 158 -> 158 bytes .../__pycache__/common.cpython-314.pyc | Bin 5682 -> 9526 bytes .../dynamic_field_builder.cpython-314.pyc | Bin 2240 -> 2253 bytes .../profile_preview.cpython-314.pyc | Bin 0 -> 11278 bytes .../__pycache__/schema.cpython-314.pyc | Bin 5206 -> 5206 bytes .../powercalc/flow_helper/common.py | 89 ++++- .../flow_helper/dynamic_field_builder.py | 9 +- .../__pycache__/__init__.cpython-314.pyc | Bin 164 -> 164 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 7014 -> 8719 bytes .../global_configuration.cpython-314.pyc | Bin 17930 -> 17944 bytes .../flows/__pycache__/group.cpython-314.pyc | Bin 29596 -> 29408 bytes .../flows/__pycache__/library.cpython-314.pyc | Bin 38586 -> 38349 bytes .../__pycache__/real_power.cpython-314.pyc | Bin 4351 -> 4356 bytes .../__pycache__/virtual_power.cpython-314.pyc | Bin 29275 -> 36776 bytes .../flow_helper/flows/daily_energy.py | 79 ++++- .../flow_helper/flows/global_configuration.py | 56 +++- .../powercalc/flow_helper/flows/group.py | 55 ++-- .../powercalc/flow_helper/flows/library.py | 151 ++++++--- .../powercalc/flow_helper/flows/real_power.py | 6 +- .../flow_helper/flows/virtual_power.py | 308 ++++++++++++++---- .../powercalc/flow_helper/profile_preview.py | 199 +++++++++++ .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/filter.cpython-314.pyc | Bin 31054 -> 30964 bytes .../__pycache__/include.cpython-314.pyc | Bin 7453 -> 8749 bytes .../powercalc/group_include/filter.py | 21 +- .../powercalc/group_include/include.py | 65 ++-- custom_components/powercalc/helpers.py | 14 +- custom_components/powercalc/icons.json | 3 + custom_components/powercalc/manifest.json | 2 +- custom_components/powercalc/migrate.py | 88 +++-- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../__pycache__/error.cpython-314.pyc | Bin 994 -> 994 bytes .../__pycache__/factory.cpython-314.pyc | Bin 3226 -> 3226 bytes .../__pycache__/library.cpython-314.pyc | Bin 20331 -> 20368 bytes .../__pycache__/power_profile.cpython-314.pyc | Bin 30388 -> 30353 bytes .../sub_profile_selector.cpython-314.pyc | Bin 18116 -> 18625 bytes .../powercalc/power_profile/library.py | 29 +- .../__pycache__/__init__.cpython-314.pyc | Bin 167 -> 167 bytes .../__pycache__/composite.cpython-314.pyc | Bin 6380 -> 6311 bytes .../loader/__pycache__/local.cpython-314.pyc | Bin 14020 -> 14604 bytes .../__pycache__/protocol.cpython-314.pyc | Bin 3890 -> 3890 bytes .../loader/__pycache__/remote.cpython-314.pyc | Bin 33408 -> 34171 bytes .../power_profile/loader/composite.py | 15 +- .../powercalc/power_profile/loader/local.py | 21 +- .../powercalc/power_profile/loader/remote.py | 60 +++- .../powercalc/power_profile/power_profile.py | 7 +- .../power_profile/sub_profile_selector.py | 61 +++- custom_components/powercalc/repairs.py | 13 +- custom_components/powercalc/select.py | 4 +- custom_components/powercalc/sensor.py | 76 +++-- .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 154 bytes .../__pycache__/abstract.cpython-314.pyc | Bin 6994 -> 6956 bytes .../__pycache__/daily_energy.cpython-314.pyc | Bin 16294 -> 16399 bytes .../__pycache__/energy.cpython-314.pyc | Bin 17745 -> 17679 bytes .../sensors/__pycache__/power.cpython-314.pyc | Bin 48040 -> 51534 bytes .../__pycache__/utility_meter.cpython-314.pyc | Bin 11914 -> 11591 bytes .../powercalc/sensors/abstract.py | 5 +- .../powercalc/sensors/daily_energy.py | 15 +- custom_components/powercalc/sensors/energy.py | 34 +- .../__pycache__/__init__.cpython-314.pyc | Bin 160 -> 160 bytes .../config_entry_utils.cpython-314.pyc | Bin 9395 -> 9307 bytes .../group/__pycache__/custom.cpython-314.pyc | Bin 55663 -> 57110 bytes .../group/__pycache__/domain.cpython-314.pyc | Bin 1908 -> 1848 bytes .../group/__pycache__/factory.cpython-314.pyc | Bin 3657 -> 3465 bytes .../group/__pycache__/standby.cpython-314.pyc | Bin 5025 -> 4944 bytes .../__pycache__/subtract.cpython-314.pyc | Bin 5979 -> 5871 bytes .../tracked_untracked.cpython-314.pyc | Bin 14181 -> 14159 bytes .../sensors/group/config_entry_utils.py | 21 +- .../powercalc/sensors/group/custom.py | 226 ++++++++----- .../powercalc/sensors/group/domain.py | 4 +- .../powercalc/sensors/group/factory.py | 8 +- .../powercalc/sensors/group/standby.py | 10 +- .../powercalc/sensors/group/subtract.py | 6 +- .../sensors/group/tracked_untracked.py | 27 +- custom_components/powercalc/sensors/power.py | 177 +++++++--- .../powercalc/sensors/utility_meter.py | 32 +- .../__pycache__/__init__.cpython-314.pyc | Bin 154 -> 154 bytes .../gui_configuration.cpython-314.pyc | Bin 2275 -> 2275 bytes custom_components/powercalc/services.yaml | 7 + .../__pycache__/__init__.cpython-314.pyc | Bin 155 -> 155 bytes .../__pycache__/composite.cpython-314.pyc | Bin 15462 -> 15367 bytes .../__pycache__/factory.cpython-314.pyc | Bin 15369 -> 15483 bytes .../__pycache__/fixed.cpython-314.pyc | Bin 5527 -> 5588 bytes .../__pycache__/linear.cpython-314.pyc | Bin 16026 -> 17088 bytes .../strategy/__pycache__/lut.cpython-314.pyc | Bin 25519 -> 25657 bytes .../__pycache__/multi_switch.cpython-314.pyc | Bin 5052 -> 5054 bytes .../__pycache__/playbook.cpython-314.pyc | Bin 16347 -> 18986 bytes .../__pycache__/selector.cpython-314.pyc | Bin 1908 -> 1908 bytes .../strategy_interface.cpython-314.pyc | Bin 3277 -> 3770 bytes .../strategy/__pycache__/wled.cpython-314.pyc | Bin 6854 -> 6794 bytes .../powercalc/strategy/composite.py | 23 +- .../powercalc/strategy/factory.py | 8 +- custom_components/powercalc/strategy/fixed.py | 14 +- .../powercalc/strategy/linear.py | 62 ++-- custom_components/powercalc/strategy/lut.py | 20 +- .../powercalc/strategy/multi_switch.py | 5 +- .../powercalc/strategy/playbook.py | 92 ++++-- .../powercalc/strategy/strategy_interface.py | 4 + custom_components/powercalc/strategy/wled.py | 12 +- .../powercalc/translations/cz.json | 53 +-- .../powercalc/translations/de.json | 43 ++- .../powercalc/translations/en.json | 45 ++- .../powercalc/translations/es.json | 43 ++- .../powercalc/translations/fr.json | 43 ++- .../powercalc/translations/it.json | 43 ++- .../powercalc/translations/nl.json | 43 ++- .../powercalc/translations/pl.json | 43 ++- .../powercalc/translations/pt-BR.json | 43 ++- .../powercalc/translations/pt.json | 43 ++- .../powercalc/translations/ro.json | 43 ++- .../powercalc/translations/ru.json | 43 ++- .../powercalc/translations/sk.json | 43 ++- .../powercalc/translations/sv.json | 43 ++- zigbee2mqtt/coordinator_backup.json | 24 +- 143 files changed, 2288 insertions(+), 881 deletions(-) create mode 100644 custom_components/powercalc/flow_helper/__pycache__/profile_preview.cpython-314.pyc create mode 100644 custom_components/powercalc/flow_helper/profile_preview.py diff --git a/.ha_run.lock b/.ha_run.lock index 3887e9f..3aa1f83 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781389664.1672328} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781531265.230539} \ No newline at end of file diff --git a/custom_components/powercalc/__init__.py b/custom_components/powercalc/__init__.py index 1f90091..2559ae2 100644 --- a/custom_components/powercalc/__init__.py +++ b/custom_components/powercalc/__init__.py @@ -33,7 +33,11 @@ import voluptuous as vol from .analytics.analytics import ANALYTICS_INTERVAL, Analytics from .common import validate_name_pattern -from .configuration.global_config import FLAG_HAS_GLOBAL_GUI_CONFIG, get_global_configuration, get_global_gui_configuration +from .configuration.global_config import ( + FLAG_HAS_GLOBAL_GUI_CONFIG, + get_global_configuration, + get_global_gui_configuration, +) from .const import ( CONF_CREATE_DOMAIN_GROUPS, CONF_CREATE_ENERGY_SENSORS, @@ -190,7 +194,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.critical(msg) return False - global_config = await get_global_configuration(hass, config) + global_config = get_global_configuration(hass, config) discovery_manager = await create_discovery_manager_instance(hass, config, global_config) hass.data[DOMAIN] = { @@ -205,7 +209,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DATA_ANALYTICS: {}, } - await register_services(hass) + register_services(hass) await async_load_platform(hass, Platform.SELECT, DOMAIN, {}, config) await setup_yaml_sensors(hass, config, global_config) @@ -258,7 +262,9 @@ async def create_discovery_manager_instance( global_powercalc_config: ConfigType, ) -> DiscoveryManager: discovery_config = global_powercalc_config.get(CONF_DISCOVERY, {}) - exclude_device_types = [DeviceType(device_type) for device_type in discovery_config.get(CONF_EXCLUDE_DEVICE_TYPES, [])] + exclude_device_types = [ + DeviceType(device_type) for device_type in discovery_config.get(CONF_EXCLUDE_DEVICE_TYPES, []) + ] exclude_self_usage = discovery_config.get(CONF_EXCLUDE_SELF_USAGE, False) enable_autodiscovery = discovery_config.get(CONF_ENABLED, True) @@ -273,7 +279,7 @@ async def create_discovery_manager_instance( return manager -async def register_services(hass: HomeAssistant) -> None: +def register_services(hass: HomeAssistant) -> None: """Register generic services""" async def _handle_change_gui_service(call: ServiceCall) -> None: @@ -309,7 +315,7 @@ async def register_services(hass: HomeAssistant) -> None: hass.data[DOMAIN][DATA_USED_UNIQUE_IDS] = [] hass.data[DOMAIN][DATA_CONFIGURED_ENTITIES] = {} hass.data[DOMAIN][DATA_ANALYTICS] = {} - hass.data[DOMAIN][DOMAIN_CONFIG] = await get_global_configuration(hass, reload_config) + hass.data[DOMAIN][DOMAIN_CONFIG] = get_global_configuration(hass, reload_config) # Reload YAML sensors if any if DOMAIN in reload_config: @@ -328,7 +334,7 @@ async def register_services(hass: HomeAssistant) -> None: _LOGGER.debug("Reloading config entry %s", entry.entry_id) await hass.config_entries.async_reload(entry.entry_id) - global_config = await get_global_configuration(hass, reload_config) + global_config = get_global_configuration(hass, reload_config) setup_domain_groups(hass, global_config) await create_standby_group(hass, global_config) @@ -517,7 +523,7 @@ async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> sensor_type = config_entry.data.get(CONF_SENSOR_TYPE) if sensor_type == SensorType.VIRTUAL_POWER: - updated_entries = await remove_power_sensor_from_associated_groups( + updated_entries = remove_power_sensor_from_associated_groups( hass, config_entry, ) @@ -545,7 +551,7 @@ async def repair_none_config_entries_issue(hass: HomeAssistant) -> None: try: unique_id = f"{int(time.time() * 1000)}_{random.randint(1000, 9999)}" # noqa: S311 object.__setattr__(entry, "unique_id", unique_id) - hass.config_entries._entries._index_entry(entry) # noqa + hass.config_entries._entries._index_entry(entry) # noqa: SLF001 await hass.config_entries.async_remove(entry.entry_id) except Exception as e: # pragma: no cover _LOGGER.error("problem while cleaning up None entities", exc_info=e) # pragma: no cover diff --git a/custom_components/powercalc/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/__pycache__/__init__.cpython-314.pyc index c5477f0d0c2bc26c17c4a18aeb889af3938b9d98..189e403b9a8a71a1892aa37b34849210671ceece 100644 GIT binary patch delta 1142 zcmX|AdrVtp6u;*_OCK}4cOKt&&v)|O z^9OKc4-B?sgO;JS_G86!Z+&X`$OMZxZ}Ni%4@_(PDA${UTJyvBSMDs%=lyNil$iD& zvKF#@ylL*p@INk&3XQ@r3^Q)d%nSo%8srko1y)?7b0a75MnRusES-H~!&ryKg`JX+ zITPCCb8)(GKv&D;JOhe_5n2KF3#%q3Xv?J6(agfK3j#^}a+SB9oRvxl==c>!I_Xsb z0tS9nG^x5UIgrY%RzM(yU$yh9b|q6h%0e^4M>$E=I#AJ&z!adGXsRyIz(w=20ySLJ zDX7^RL5(zUd^3xu!){DI+yFnw?T7CHJdi7kwgK*AvUQGPH>?>@%>muaPQAX&>@h}2 zS5(;2X2m(;yD2$wfjBNQmXf%nDh(rMDoI9JbEKaY6PxGolr697uG9COv*ot4Wi!~n zJ{aC~4()Pmp7_0U=>4|y+8Ot^n*7_&p*Y6~IdO&(bfoDhF4($6GkJ;$jiu8*liG~w zzh)XiR2YJb9dVUrLkVwejM9Tt_y3YeaSS8umQb``-YK;L>dP0vjM4I62^T8nz$y>f z?*LfQ>-ZcT_@|=*(&Sp_OTc$ZCp5TYPsc`ArmVPhEb6P<((g%30NNMn0bC>VkR6g= ztiB4OL+$WQEUtYe$>#ggf2(Z6){XPEv80^J>&ENnHnIcwTAg)5F^Q>ZN+x6QfudoO zY7QbNsmm#2P=a@$#m!$VCAG{$i$`1*QdFJct0#)4LJ-7gn%6;2jE5j6#)Jx0yC5j= zu_{T`9w5903FHzesCfK{bVm~jkBtvwkNYWYVwh9(*?s)NJ&Bin9-OX^K(G8q{l@^k z_@U=Mynr)}4+zg4li`e9>HQsuN_;%P8*-Wd5)1Fjv1Sbm@8Npz7jl+bZ2;Hs=SLMP z*lv3gU`cj$ghZlConaQX<$t;)m89+>fS=^Do<~@s(9;C~yYj2O$5^79&vpYVuvpO7 z3ZLLwZ$7S9Y4J)=9e&*x0RwvbO*qp38EDZuurCTMP5c8a8tl@V{9F1%Tl(x>0bd_% zvh3)a4kFof!O5m9UpjncDIr`D!IvtoNIJaQ?nQs77_xCB^bMgW{5DwRjqq`S=%JAl zEZF79^E3*(Y(BLl!e!h(Go;^Rd0-WWU{8yWjr(zQT##{mN>KlU-^wxfY%`q3fwM>d E1q*9SD*ylh delta 1333 zcmX|BdrVtp6#q_p?=AMlt1tQh%6OM;P&Sa)pg>E3sfb})9E|srg(-oN>8b(Q!XJy# zm^0@z(_oemmpHe1$;3w%JLp*Sj}=)~vY1Vr(M?BenaT(;cy2X*$@%?$=X~G!?)hH# z^eVi*1{#M}qh!?jC$kQ`c~LWIhD$hQu7!MDGk=RWt#RCdyfqeowWi|(%SZT1}bb!-RJdgk$9T!McNZJAt+M<{eMSE71Hi49oq4LDb1NlaeT7Ff*)6lDG zo+4M{IgbHf?J2|HRt0{URs=taD``Ig+!I6T-vj)DQ<>)|)|I7);tgp(@(D)SDocRG zhy;sDV^~i7Lu^u*Ck_iNIXkS7R16agk;f9mPqI5@Peqd_DfvF7hn38NZv2||PsOl%HIbfGrt4GvOwH;wzhDM%emxv0CxPj;B&~s31I<@ZvO+Sw_l;R7ArrQ>h8$+f>TrbHQS<$k*c8I8j_=JdoX=>=YH7>;zuMHiOR-yA+ z;T`a*xKnwg{2X|-f}f9-wBF?)nqlCL=VqYVL{1o+@ilUhiPnPzoqvtSA6+ODokS`lktqloMebtH8 z2E3kCih;5r*d>;fp9R>3cPoB_=kfZE2ZUc$itwU1Q1urOJy*>E925I%-j%`IqTq>< z!6}UOE|c@O*8y-Aa~dKvz|y!IU|Jm7*&`4wHuuWlrnsX;A(3Zy2Ebjhe@~H&=*+XJ z0L$X+-YOXpzi$u7HUQh(>)|pgTT?JtrNrez4c=*=06mU&n4>58UdLy=^uJJJbLS&r zLpreY*wob$o7%9bj$2fl9`bU#a+TrvuIkc=N^Vg`w9?tE4dv3R$6O^fHt?504L$zy z4ANIJvk7OkP-78hVpWp1K#dLKyL%x44|jh@*wAwVOyZrMCwZbR2byG%D~|Nje&vce uFI*PjB;Gr;Kjsee%`@Ms4BLlmadem$&kP^sr5E^TE;@W3IEaJ3+M`^rjeZsD+IWb~eVfHRm=k%2*mA&fOZXz~Is0~S376Q;?2T%L@I zlV@;QK)9EOAiCg}(!qb7L-8Vq;sTxPN){KD zEUs`^UdYP(z|6oZ_<@5#P;{~buLJK50in;V%&L4BcoaWyP441VXS)y)by*>L@)lk< zRoKx@WB?K?ls|x|4Y6SAKn+;1NE@g&nF*u= z3V4BZGXn$b=VL%oB?kG;nH)ikj9!!Xa0W9fOcvm>U@~Ex?8D^=f110m-8hoIr9iGv5-%q|Gb%UNZryDT02C8r47(iWPxG1H*R) zHAcygAYw(z2N1ObLY+tf3U1~SHek`522=gxB~Zf$1_l)mrjHB^u8i(XJAzl(UQ@C7 z4B>5#60c@t0~?+(`L@K$Fp$bCt}$2QioP%~lrp-(v;oxvWtKB9XI{h%l)jPJAaUlCDV5qm|%YERvT(4;H&Nf**V)|4|kGJOY8?jYMh z6i_e&NZpytCDqHgWb+KET1Lhtlf`5fGOpRYLx!CZNF9(>VNwMqhObGCVvM36jDX>A WAOsvVUxOG~83n%!urTU_(*^)lEvKyj diff --git a/custom_components/powercalc/__pycache__/config_flow.cpython-314.pyc b/custom_components/powercalc/__pycache__/config_flow.cpython-314.pyc index e8a246e71cebbd69643b8b09a6111c318033363a..ed5d9083f9949f7185de173877bf0121fe2707ca 100644 GIT binary patch delta 10831 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 KEdwfTAASKrjcEx0 diff --git a/custom_components/powercalc/__pycache__/device_binding.cpython-314.pyc b/custom_components/powercalc/__pycache__/device_binding.cpython-314.pyc index cef119800dc96a74db2beaef1898221683315bc6..c074f8315f343fdfb29ef94d55ce6dff6fd984a5 100644 GIT binary patch delta 104 zcmX?Yc;1j#n~#@^0SHdN(9de#$jd5hV+G_jFnni_Vr2UWBJOaBe_#gEPlRPZumb5X xYz!g_3skNOt9@VxaXA>+1SY6lWfl9#DYe;4cr_EF(B^NVXPG#Fl0`B=!vJ`X8c+ZL delta 100 zcmX?ac-oLxn~#@^0SI!Isb}SF-DGC3=>L1E`@$251lfX;K{i diff --git a/custom_components/powercalc/__pycache__/diagnostics.cpython-314.pyc b/custom_components/powercalc/__pycache__/diagnostics.cpython-314.pyc index 67ac3bd2bb7fbefe9a0a9fe8ec0bc84d44e0d72e..fe05aa947edbb32d7e3ef497c5222a8dc5c965f3 100644 GIT binary patch delta 297 zcmaDSy-Au^n~#@^0SHdN(9dGq$a|QHv1sx|CVQzM<``CxI0%F>m@qjoYGx)Ruu@MmSPj+Gn;Pbo1U747h6Q7)) zmzJ46c>&9P#zm7GSS7f0T@R#O*7d%w?mc-vYhjcigN6^|O>xVQ91Kc1j5n2Xe-^s{ zEoxG5W@KE%=w!*YSP014%IIXxx6M%m$UMZw=w!=sh+753)@O9GXE|iR1!7w>f=F91 zX%8ePhp`d7o2|PcG*+33))N?do+$_R5j|Bkf_fLcX diff --git a/custom_components/powercalc/__pycache__/discovery.cpython-314.pyc b/custom_components/powercalc/__pycache__/discovery.cpython-314.pyc index 00170e6cbee7863a53463b91c7021148afcaa39a..ee2be8e248c230bd51d241e404398c44e7b31103 100644 GIT binary patch delta 10658 zcmbVS3tUv^mA`l1FNW6)Fbu={H84C>6i`Hl$2g;iI8YP?#}P&)LBKng81h(*F>0ep zH7AWpnpjP;t+v_5{79PTuZl5gvTe5zY%sS;wfSY+rr9=-NZq8}U-q2u0&21?cIOBG zd%nm09_KsXdCh!uRPpAM3Uj_W!ocC@XMZTV>4{B;%?W(t;hbEBT0Fd}V9*;OMx<~X zIltCt_C|_P({wb_F^tyvV!am8;h!*NZ-SWMwTU)wqL|3)3_iOzNlfxO zM29z7OlEaPUy3(XOl7p%m*!3HSEO*>3^9|{n0#5@!E7;mx;baMIahSDnh0N>H($(W zwAttK7KjB%^ZrO*p|?mZ@)nE5tQ_T==bbOk_bw0@cuT|*??Q2*cagZrTPl`%%fvF= zNTj!1EcY%J7kih8OV~Tnz6$SBajCZwZspAl-fFShTO-!6rWjwkw^poWbgZw=>k&PS zw)mENy`tCa6Md{~^{w!(6jyrd#d_~5ah110Y)IkS^^Heic%Q}9-dMsD&pA$9(;kt= z`QzH1jZeun*J68SfweBF-aq;_42Qhr%n z%W551n_Qxpu3P6CGC{ehK3Q5-)6hmUy_7DUW36Tm(>Fybjw9Ny3p_ZTkgNj?C zlu|^EyS~2KU9&RK+}c<-^cRJN=Z!-~RXHCkMF(oT!X3T0goJ^n-hQDY)C}#rDkD#7 zKce|0)wWva3Q*F*7?*~Q`gY!Yi>QM7Rsol z9G=yc@|EzBE?2#Zy%ndQ$7e61wh9C%FD%7Jm^hd&R8ln!&Y%Xi8(e(V(4&T2MV*Ig zs|n;8iK-!}C8#4l`U}GY5`ZQCu1=cRSms8p})I-7;cV(AI6$mH)G#$1Hvxu z3Ky+z<0lzjGTSHARGiRomgEU7BXwNLq6s}C4P18KgprXZE+%dwf{|vo!_H)Ex+5l0l-;iA==#aPx!n^zK_N&h8a^aDe6ZD;Me%Ir7F3;iR+>Lv5#Mo!D+er)`iM0-l>Dl32znAP9#1c zP#lZtFclJ09f=I8?ZNSs}U1C;ln8%;EzrsNAL8JG2Hmtnu^9bN8ZET_-ZnvS@$qz^H)j6tRb28BdORAkv zm04CZfbDb2zyVt1j0~vMS(6;XDU<`3C1_hPr1ef7~RjA^(pnqv^|axCBv z!1IomH{Q^shp(N~qAn(hD?TQ&2h%(q(-MRIIF7W}-5eM3S&R~ncO=8d$t`erUmQM- zU|SgryDHRRPRWFqQ;ZrCr8WgbtxSSIN+f)m5(^*OjBqfekXOMgDNfj*Y6id20!gV> zMGFUaRas!kdINZNT3~f*Ik?h{RK3P%rMph#p`BK!Sg(ik*{nyrcE;49+!GVnr!g6J zv|3?ZS~9PLooQ+OLO7F>;>^AON29bsEy( zf)}1j&vC3FaudQyUY1tkOt#GTz3_+hEIngp6jsA}dx~z&d6WNLQ_KahXMD1#+Un8r zXXAMfuR5FH&c?!PmApr(dTp6I8Vj#S@gBA6_2_v>zoFy_)rcfK**A2K--n}_q<&t> zv?-bu>~MZxXMr#CEU;;X!7{_CD9-a2j_c!F_(mNJ8xrA5g9g@TeF|;aL7s=7WcQd> z{x`?zxnz#c4=Qsy4U)32BhcWC6w2V~oQztsc#^)qr)x)lDA3g@^io+F?CfL)QBrOV zi2`XMA)SB}fTRqIg3w5mqN~%X68a%FH%G$+_7Lied7AHITlfvwnH!3>I-kxs(e=Z^ z7c);gUviFFYhfbSm+_8i-e)#0u9p8iOcr2wwW&6jKO5;*&?f zp(2~yGr_NEQMc%4#>c+D#W@rsbO_Q&-2=tI^=Sh5ygu|0Ahf zTg=P`G+bvE8QDHVn{K)eP8C@dek~Lf+il1@Ki_9qX6WL(xFt#l*Tqkp&0ujwtUzKR z97m9}{XIALpv{8Ei;ugE!ZB8d*CZ(7wWy;~gnGm-abQ{vLEpUfQDmuvrwRUz z;71_MODbAPWH~`M0ST<63<*M*ZYMb^=wZ@F)=q~ZYkp1{JMbc8_(yo;lV;B08cQk| zk1KdPu3*3CJxA(O@h9R=*0%ML%uz&t5{5w#)pl3}@S@ei>hD)12j~{;VmE zFr9jy&7={|mM=woZ4uSHRz@}FOj@MRS$U%48D`kn!)~lIsLy35)T!0yDyh@!YBkop zZcri(DV24ZObsyh*oZxhTZKgZh(Nw5@a>Y6w7LAUkKtD(Zd42L3)MXD0mq_@JaX(w zhtWuE3N|_o9HwhR%$K=71UD_p;y;E%i$(_7f;`y#MN~*A^oO?MwHF5JdJtqK#nw63 zYOc^EDG1D^QZGjrT!A=Jx*WYKB;Kr-D`dkxYZB2OT`V>5E5O|n3;$DEV#wf_pz^n+ z!=}ZylAcsq>IzYKZ8~-`x(=F19-eFuaK=L3EN1bK8(ybRzIG(cr@7K~z zlQf;Z+k;&_%*^2N5I-eTPjGuEuz4WRAI7`!A6UhV5{7xS0v6^?@8{dNAIAerBKW_8 zdCBosw#t{NKRcIMsgWZ3dUu9|K%daNrK>w6ETJw;6J<1T zhKxxglIXtvxzeVHQo>D1rBJoZ&zAr!%T~20T6lP6*&5yr5mh?ptIE}nk)DS&0SiS(aG@%XKTCt^G0>L>MSsT* z4Q>~2hG$!2;ePibZ3e!hX-uKv;5Tlw1J9jYpBBxuY1qmP#2SXFpsz0BS3+gAZQ%i2 zon+k7)zcZ+-rE`K4hscX!{QFfG^5K0Q1ax{^-`k;_Eo1?B;)k?iI@-(c2I)=r>m=0 z=w*$al88-OFlcIn>lRHSDx%L8z(9ybxtMqCc#^ z5EXMF%5lM-bs;_HvQgtOU*Rq3faJf(azrTmcTLTt+0vH7Q# zkC(KJmb8ql?HDiVe7mIcT}u+^YVR3b$l22-%DAZH5uM}HI-ZMlNzscwHQ{)lhud&T zyDc6K{}5ldRCOc)sb_c(r#fXs>eU>C;SBVAHN%-yFqqY=+3atZSp zz0}QP;SG)s6v0}|M@tMcy)H@lBF6m?gSNRyjNBArN4H^Pa533|Ml#2zE-H?mrx z3cGd8`ZgRl^j6((`9ZqF!m|jdDcSK@Lbr-SFmP1{FdG)coWwlrBdO~#m)9(x`t|o* zc%BB@L%@O$<~;nEib({S2$E`TeO>L0<&out;!bA#zoHAyg4>&^VcRzV(nP8dqT_UV^a-yMFm`VxYAy}Z3UB%-g*N5;h~JD?j}v~0Aek6f0=27GxqS_i zLI_Eka40BrY!i-Ab40JuwY94!*c~8kE*X5w8&>=4YTQkA%uXTW*{Z%~qK2WWK7(He zo%P329bW9K(6B8m2Jfn*wz-zNnIt0~rzgosQm;ZwOE{9bSSDm~z)rIt6H<3mXxuq; zW>t{aP1{%vEM0x~JT~)F*yBc729vAH_z>hZW+J~&x?Od@6l1T8fUCq-g5lv%f# z1Ru+5D6uI!br;hIk_|@I*x>tXs&TwOuBk&_6#FX@&tMm{gTKNDhO;!*j5&n8{>sFi zw10|pc5x>>Qn%L0#ZE8Nvoz$Uc8 zn@uGPG*~AY!#lggj%@*4_^r4CHq750VE~Gn<6En6RA+>cO@pIH&Ixe{lBQ;LgU7dA zGSvFqC=*T1z8b+vt!PF#A%~!tAQwSWto94}RAO$;boll+IM!^Jx2f)ql=1fp=FIR2O4tUb-2NK^7_;`8ErS<*xT? z+UL9!yNJ^i)5<z~Q&%Fpb6U>DODY;#QgjNEJ{TV8=?GAeT?YJGYxG0- z>dev6{vL#!wQ2k=@U6Y&#?nrEXaO^6Cxvc~kB;bBZY;p6M~ON~Ra`*jwMb)=AuSE2Sx zf*naI+7t}LFc%Cv;&WiFB@a`jlq9`#y%m17A{N#Z$%!TX`gn)Jg+9d0$FID$)D{Oz z*V`b*9XtExpEg=FEegNNl?cbSTj2D1J?2Q>TA#YCLDFJET_j!q5{@Qmw{-XJM3)SO zG>qlxtRzv4Sv^EiK0(e*eg`r(WGygKnY4yvl+QG{xmOUUGYgWMMhpuM;*orvDYtO@ zhU`MdHcC8@(|ySp`G00EFBC1lZ&?LC0k^dkmXY56B5D=6 zFTS2Fy{UJk{=@G9Xtk6mc?d4G<)Bj1wf|J9f0AmCBfwubIbdp2Ueg|`ArmI4nb#r| zQpKOCLMw#&x`Q1dNyl1+p?M9wGM<}Iseem)4|*M0K09>biPI1 zneE9$9?dCir-_kve21=Cl)7w^;KiN^dW@_IvZ7Emw70{pSYL>JojSH?!9{}~!}laL zMr>Vuk`^^C3*4!fr0fp$ur1JVo{lAZDm zIdtz6Yv~;5l=IP{gQ=T*xXWcXZ&nV{O+|0PPFt)-y1^MICn}i~r(ul{O^`}J2~$}D zNND5}br8jlUDMx3-*U=#nvx=NvU67W1uND`(ek}!3A|7MQhU|yWPRlC4C&@OuXXwv zYZzCqGK6>`UCmtw$e9!<3=HcL$nnR|4D){syL(;ydqdCk7V)yjBl{hUeN27NrIC#a zsNZ47cem|miT=8p|K*O%B!dDcH{;~HIPxhi-@4%RIh;4+lt->n$>M0`x4MG5M|q$GYR;p7wH<(*A@Etqe; z-`+`U{w_fdijbrtK^CH@Y=jqXEhtVVl8M20h^skZLro1Qkkz1Jg&T=Fc=-Vjh&=CJf@qCZ~V5 z^^w+b$HGy^!f{8vz@4fe^ha zuVFn=Yz;TArmEo8JBx8RcZF?8^DpguB6-@vZwW|fiiA4{?fJ8-6=*g|d2?t$ zQsWoc{h{gK6U^#>R(Nd?)jIrXu>0EOZe(K-{C}3a0zTMPBrg~8?|Q7{`ZdlN2{s2C zehp6am4Gw=f4$3z8OW^NwC`CqGIeL8eP1;UjEy%GPd37DXt<%syC38EN8#x`Te8mJ zSQu20ujN!qio#-fU}E638--C3l-{#~UkkhMnLnQ?DwujkU2yi%OY|ms zlEDuAwr40vmigQDkJD z$_7uf(7h+e6~E-cI4$RR0{oa)Pr2!vqfyJV+2vGMPqDM@4cvgggQZ6@QE%=`i7isWSXh zq?#JOpau_74Lq(~VAmu-<>#s806~y|RDw`NP)Sfb1GLky6<^rPO)8XX-4z4<3}q@C ze{eD7!mrw=O!y7>lnWZ}&ES`VaBo2aCNQU3c+3f5%1aY-S)o-MK2st!LDWNrTBlDv%Q(7~q)$nes26K7n@+WMPW(%LC~~ z@-JkYVg}bin=z8gQ3TP@crd{jLnK*0!2&%8?YtH4Jy>EaBSw-ZC?&zS4#vU7gIYcg zTkJ?_KzE;AlS_?u*0}6GjV2yPko#s%FAAeFTwzD=_U*kra<(~et`)p@pDkAoccclQ#_uDlr z(kOzW`_l%CX>3Zl$@$j>^sXLb=6d8JeiOiCe-AY~X*b4ezlJEGYRnmP>^E6*B*$du zdU=v`6&S_zcZVtkz%lTT3;zOPLdo-d!TVhL@42FjT<-f^!ml~=uQ~liF6BQt_xoJU zMJ_PP1uk-{M!8iNxw=uV4nDYlUayWmT8wr!)q>U@9?^FSwkk;u8;Tz0Bd` PirKB>lRo1J*`4@bB)_@D delta 10312 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 QB$@}qSn?Htkn80C03-oep8x;= diff --git a/custom_components/powercalc/__pycache__/errors.cpython-314.pyc b/custom_components/powercalc/__pycache__/errors.cpython-314.pyc index 7b62d31b0f48f31e9ab54e42dc5d09196840b340..88c349becd4d61517dc83fffdd154be6423306ce 100644 GIT binary patch delta 20 acmew(_eYLfn~#@^0SHdN(BH^i$PWNMj0LU$ delta 20 acmew(_eYLfn~#@^0SI!Isc+;ixGp$3l+L%nrBZbHU1}8l$xMQY+zriS`NObEog30OC1IccTJDX5yBTo zfUwWPD6rFpYT(j@&{1RhSUJ&y3HO~5p3GUp4w3-_x*1W_&|!`Mm#KG#LtLhv*OZWe zN)|ZuvwR@Z$z?kLk@ndKSkCm^HEqIXQH#V>AtD!is)83 zyx5SPsl-flVq~gC6)-O8iC(-N9gn3N_jE<5FBoLD<^HNOuk6$UTyz_ zm^}0ywrRYh4BIT$*$*JFhh6gkRyq`V00J$CUV)vxJbVtIj=H;}7@Y4;z>Nlc=R|yW zVHeaD2*SEprEn5g$LO8|2t#WE}K6 z<2|F7j@*pqrJlT0os*i@q^6wYS(7|@sR~cYB$&j4M6C8NX4iCBG1sp0wSRx^=lAIo e5;n{Dtx=iJ0wQZW`kK=|>X(=5j>nCAbNvUR#KvL( delta 697 zcmXw0Z%9*76u;-ay|+KQbWWef#EE|>)P|O(&9&MyDQX#oB?Mx!QnW~L(v}}W3aP$C zc)#?)K+uB1sJvo9_TjCdK?W1vLz03BA*>H=X=PH-xkJx~-|u&R_i*mH_h=)qJOcL0 zEp{8hxNv`4@90NiH>(wZ1RJaASAvV{4g*95;unudia?cR}fknJi z0Crm88v}v9^}PlMyS?`uKq39*Z^Fd8FAQoi{(rb*JrUE&fiG8)I>?VgP28hhw92X_r&(AWtc&Nj&Im>^>!_cV! diff --git a/custom_components/powercalc/__pycache__/migrate.cpython-314.pyc b/custom_components/powercalc/__pycache__/migrate.cpython-314.pyc index 8182565363440b6c6549017e76a03b22efec3ad1..4056d8eb7b0b6b47aebeb67fd15020345085d9d6 100644 GIT binary patch delta 4961 zcmbtYeN0=|6~EW-VLyLhzA@&*nC}<}B!ng;q@@H%AkBcjP$!`d9$=R^1~w&xb9&|RHQs+_j`cT=dtwV4AWFt{NaIZp{VVX>_#0IZW%T4(2q`6=jKs2AXr*eV z-cAkbPD9>1dZXH6^y*a+DFd zOa<=Yrh`^ID4d;;Z9ATPJ})tcN~JxYk#BT;)wZgO;XEEs&=8)AWpiLjqVCI57Y&3Xeq zR)eC>r0B$CaBOta>kBC3K{$QZJ06^VE-(?zO-jI7d^06lkepLOaJ~}LCt91F4osgN zoecQmdMf^DCCI$vh6HRE%r#DtHK4+cNRqFph%l_JbvoG&)oyW`J7LJt`YBPnE!(73 zseDn?q#E^o%T5?nELLk$o%(M}L3mZ#wv?|)O?M5}3l*=vv?>)Zv}%$wIV@Z7X;M{k z*t8VVq}IElv?AJ;Mcay4zbw`-?f-4(4YB@~*#EVHu={?ewQCo>o^zlQx^eVS96$us zL@#cja&yE@)I*{pEDO!LDsow5^)Sm_#3{ngPb+IsiC-vYmZ*IC5dLKj= zL@CBDZ}C~cEDs?g5255UGHH!RJY+ypmHb_%{jLnw20O5PB&A3kK%&A-aK@#JLDhul z@je$g=l3bNO)Yp71@|OKH!uonHAime6ueh7Uv#m4aV&Bu$2E6&)sS;xSTmGs)q6hP zr&agdmU7>=zG;2;#l_ZJ(zbPh)EuPChVqm>wz@@qQml#lgLUDVc;3V8e;D<_*NEBw z1mr}4o}iby(E}&w(ok%iIuk^B6M=FlyblaQVKAYM+GenzCrY+@0sz8o&hw-Cc~*|- zCLW#YH5G)V*tTFYU=oIBhhUH}JEDYDA?Za@flG0-XL7SA12x_9XlsUqziB%#VEO!_ z_j>1hFP>aF5Xol$`fyZI<}9?0@$1j_9$fd+HL6Q73qK?Vh) zW@P6&P$k|{SWZK~O0x^0G!--@fu5>EZd|OxWEy*(CSsvk07qF9^XMXvs9rD3vn6GB z3EAWz6qFeXisY!-x%pvPmw3FKJOy>cNE}3lOyLrS4)(i@4C`hrqOgFmTqJk~!`j8N zkYGCE?oN1Gc-}vIUu2<284_;I)4*L~ixEEaubA;#^i((YBMb;BTL>)#O8 zOvIS^w)CcSA@73kmQ=D~hUt`hw!(p^onrMP#o`DkpTt83HLVjM7l+ocTlDc!>Ohd? z4daI6LC5is7Kga3?l(veh()+6+#id&L8D4dWu3Zbgfl`DIiouWes}?5iB+W`MCVDm z+zolZU-uL^9X)$Sm=R`l$#_nJFg>2n=*r`A0TPRCLks{swo5=^?(Aqr0BXF#Fl93X z5SJJ$D@Sl*07z7TLK~}^POO?Zzdu3Hj$vg2=r@IW!9tm4sJ+|XyV8DSx&6pzBawE` zO8eMy`)bF-2N{DbAO?Z)#;*z!+@bH`>vkWI^~FPr^z^x33c*KhIyo?&E4c+L~h! z!W_o00&`!jhjAju7+VwH87Z6$VqpANYYB9tsXLm|s8l@;NQLK4NIN^1YSj?&aog&Cq>Y zvS3EnavE-BN{txMJ(Rz_`lWTC)`dGY0eRT{Nl{0#8fnY5x3ang7aKjMQGa2tqapP#_A0P}xwzW$1DNJ7@iBnz@#0T1V!nF0U^vWz z_y_~?FbjMF>q!eZ(E>oOe0L4I&xUxXecQ+T8-Xdl`Vb3T;yG5PGlJ*ZpG$s1)z{0y z`ZK7TwoTB4VNi zr-D=dQAG)0)(kbB@=wmd_ct@a8hb_kk+Ytf)$7hW_3zFX46GVwYt+$-J>gnp>yUIJ z;U719k>i&!UWa)e<*|s!W#~GFid`mL8y>4sY5!EvcV^Q61p6^8fjNnS_F5AtrFV$q zPo(vaWH3So?~ukjWcMA?bx$mzx_cu8=>tg{p~vYa{0lQcH{0Mda?^reGMg5-Rd1HU cRcbSv6G(WQCI{7T9)SOu^!E-^00GzSKamK&wg3PC delta 3174 zcmaJ@ZA_cj6~6a<{e^AJcla_k*nHT07!oKUBv}Kb351k_Nem%|#27=J0A}}nX_Bg& ztFDt$ZCaDwN+VjN5UE;MDL*EWAE_qoBdXSP>kk;tU>4C*)l->0DjvaX!%B=jen)B(RUGI zB|e#NRoeJpY^J>$zacf;r#S%Xgab$T1!L$*S_{ShH2sWcZ-mHE`d zdvx~w+37Iy0SaLtfR%V9pX^V2sxh22CbzYNzbBnY0nOzrw!AK_KR+EqD^hWZG1~sj zlzm6;jBSD0u)$2Z{8nDUz@9{(kmP(CG2eoSMf;0V-W};LN$G9FkQq=01XxoxK~{m? zmg;+@mL$pk@{}z(2B=Fk4@r-yOxY4+qL`FuUO6?UQJ&Kc1^uwvM8hHe^nCtwRDb%v z&2lz5RzS#{-Y36^9iJX|C=tz0dSUZ2hz+^dngtn`{+Ij?t-m%oJMksR_w7P{dTfgP z{s#Wfys{4cX~VqiA@kC)JVGu(0_?Kila8zRH>Gt$qtqw$!V)mCd3nx&OG#{CSvq=h z19c+jYw4WD$*t1(_obsHPHseO+)F!F|2{kr2U=5>6eW18uub%dn9oJ3Pag$Bc3D$f z;{R~y^v~U&7q3Z&Ipm~s6>B2>?fkp?yy)RnoEUS$_R9$iR2b?6KJ7`4!vCee zB!{V6`vfmQ->GA6DXy6asDZftjSy49ky*E$wSbE?1H|R2kjh|J2rEUv;~kI6bCEe# zhJ6!4F+v4`9UyK9&8qCmx$p#YqD_mCg^+`Qmy+coSP;wz$X;9xn( zSP9Q6YG5`Pf;7WHmDva!5Ps88<I9ppr8}wqBEOyxIkx z%$)bT-|N0MxKR0Lvwh11=1mJRWMK~9PQQ3zG-j$@ACIh!D=Xv5zX+M9UD7tZA@r#9 z48;j)DGy(FiHrdGlUv>=oa`_MzCB(Y400KQ)f6PtL(y4DxwO$Q0)DDoT+M3UE$6pAU9c zM#h&%M(>{Zt^C=@^1!Lp=F^Z6I?@T1JTzNlWgf_g|0biPeLD+R^UbzV2=Uau)2Z#% z)BA>3vOs)ZSbXB3?(?RO!_fJnp}3do{-Eoi(BbyN*Q)V6v3dYEWep${yoL^j<&NR| zEntv;SXfV+mVQ@wRIY=8cop4QLNF{;3`#R$_$I|=MP+fVGB+JoSrzw`IuGO15jW2S zX6GjZL3N&mz&{h22u;Ve(~(Od#t!124*|GM&)uM2w4UTYDSeB2xXD_lJq?DzrGwTr zs>3&ixp|STJX!~8-zO5o_n*Owyz4qt@GE#f@ngh6bz2B`zPE|u%Tq$c&QA!r9G($g zhQ&UZtYe@Of+YeTSIxExR2dw^4$*v+59GO*9@}1~8Z2krS-wHtA5{FPSrn8PRhpeT zgk_jvb!SEd2mf`2!z4-xyyJwO;s23F%bjoz190eO}4Y;*_%i~#JmyEnkEAdkpOIkEz9cXSg)Y;Y(cCoc%O98rXO7h3KB zEeTeP7?C7s;F)hvl-*%)uHHlR09C+Oom@NtN=lfkdGREI5A{Bu2(`(948cp zA7Dv_P`ijQjW7e?)+UZlf-x|}b6yW9=cYqbfnch-GMryQ-m7(3TA`{MYicc5z#u4i z0Ll}7q~`YpNj5u6yYlIMTPt9nyNO#|zCH2W*CR8bfTD!)5l}tB2%KjFkGe{ASh3kf zzUZo@M*c@vjm889zU8{i6?aP%Zvm@9z$(u0Ff-gohSPsS1fSy8#SMqxcFa$QzQf>@ z5xkDdS%6KMQu>J4{!Uu{N{+0MBacY^BhvbabUo9QP$+f(2c8Otqm|HM`c#Ks;7^CC Tjq09u!q>a**$E1}EN1@?gRqg_ diff --git a/custom_components/powercalc/__pycache__/repairs.cpython-314.pyc b/custom_components/powercalc/__pycache__/repairs.cpython-314.pyc index cb59521a8786fdb044838e8ea46e21d73b29458a..e78fb80ab6f1847c5331eb3501ed19afac01c70b 100644 GIT binary patch delta 517 zcmaE)a#)2|n~#@^0SHdN(9fE_k=K+(U6Zv)5GZhqG5(imN@j9Nba6?MjslQ~Rj5($ z%g;+K5&=s8nq0){y?hF+Qb4}+w&AE|WNl_= z@JXM1fK!gqVDf#=av&MbwMg6qq}L2Ym;(t-_98_PTX(Vmx0SdlkXg(QB-GRtK%hth zB&Rt!k=uYx8YCAuc{+ECqJ++hkn6hk7j^Bg>$+UjbvY7uRX5~9Sk(2fgo|Mbmn9M> z+w&}Q7Gw}kW_&2?^|KgcTa&Y`8$07+dqy{AmLm+@K=u(HK@cg+=*G%&L{1#UHq!=@ zN0pR-; delta 514 zcmXAl&1(};5Wsirek8luO;@9-mM*ELYhxkSZVIx67LkS$S|s2XB@h+Ux}rtiHV8S1 zsCO9z&mIB_f*SCsheXZ6gFz%35NrQ}1ccy0aRwi}-~4#?AUMg<@Zo^z=Ye zu`NFX7enPN#IUVg-Xo@P%StJSsLsKg>Jqq(=jubidcOdC1 zM{g6)=}(e#R3mbXc|t}Llk6^9hM)9ELyh3NDL!J1bJJKhW>uasynv64dueCxMXu{D zcf94USLk?!kBeWt^-rZs-O@&u26%!r=xir zng-+zxfxp^Z)uP>U$j9ZDn`B>{A-+3ynE2iVW}Jve%%Yn4KU>XQ;Z+Ss z@q<~hdyyavtCezawKo!O3z5Z>)_Th4*=Gs7WF2w?##cSrAECHo+4#!(tFCcBA795O vjaAkx;|%V`k1It|zj0u+4N$}%_5_shk9}hQH0X~Pen}LD=Ycwjar*ZU)@+45 diff --git a/custom_components/powercalc/__pycache__/select.cpython-314.pyc b/custom_components/powercalc/__pycache__/select.cpython-314.pyc index e11068a87d95679a1dddaa543be8ce30d033e6b4..15b2c7d420ea3c78b3788da2971bd788360a0cc8 100644 GIT binary patch delta 81 zcmcbmc1Mj@n~#@^0SHdN(9im`kvEc4iakC#KQApay$B>;qy!>3fy6ET_{nvgg<`zo cA6WSqtv|3aNa}oG2eCL9#Pl|^a)ojN0Cw3FV*mgE delta 79 zcmcbkc1w*{n~#@^0SI!Isb_uI$Q#Ki!Iqq#mzJ4c1QIS%0uh`*;uhcJ2F^k;9rKCpvW91Nnmo7uTSIRO?-67~Q9 diff --git a/custom_components/powercalc/__pycache__/sensor.cpython-314.pyc b/custom_components/powercalc/__pycache__/sensor.cpython-314.pyc index ae1d65d34417c2401e6e9eb9cbe0323363fd9fd5..d08aefbd63926074416c6d84f9758b9e7353ce80 100644 GIT binary patch delta 13205 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 OOI7}#nS+<=BmWOM*Cgfu delta 13463 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 delta 19 ZcmbQkIERs2n~#@^0SI!IsZZpd2mm9X1N;C0 diff --git a/custom_components/powercalc/analytics/__pycache__/analytics.cpython-314.pyc b/custom_components/powercalc/analytics/__pycache__/analytics.cpython-314.pyc index 2b5aa2dd210ee4855b948c6e0bd52165264206d7..69222e3a52861e7e63322edc775db0376391507f 100644 GIT binary patch delta 869 zcmZ{iZAepL6vyvT?>67A>707am!;0PX>Ri+C8yhb31cdi7-1pih9K$enFvCdP)M>e z<$;8VqzE#ye7Qu?ha$w1UcjleSOf`vF#O`s)cO#eXBPCO3-|Y&d(Quy|GoEl)?KjT z0zoT;hj4hkTgz+TubCD^mBK4?oNc$acGF)`T6$8Is3_XtWPJR%O}e0p4TJ$285$=| z#7zZ)i=IhXg%Qb;C;+&mV@crvLsXZf#)tmoJM!2Goa_uhq^rphT*xHD6fMq3g=u;^ zV<7bjCcJ!y!)8B6{-c)oOe4TuT9I`X9!m>ZszA8OOp;vK`vPyL64B4?a8Z1mk6}C> z7bz|GeE>Y7zPtpOq=N2p%rMGIA%#muaRXOXoDZlz|6 z=OPooW@{&N9^^cWac7tN6T>hXb|dVwSpUNrf&G_a%`zAh5HryTuiR;Gb#&6#dOf_N zyZR1s3`OK4vkO9`tJS8xSDB@K1`Rx<_Y9>5bYB%SJ)&VzG)xsdEtxJ^5{(|Q+AUUl z#9FslyDZjk@~GP6<&hy*_>Cy4%wOW!pInsI^Jf6w(A$ODXtphXZT@WCJpEFb35$}f zs0{oKUa98LDi1H|L1X!W5p3{=v^aY^TbrFNJ$53;1o^Su{v>=0Xwo z|9hs4SMBUe;e|~Ov8NlPedam&HWP9{k6KJHC%G(ZQ1BB|-Y9dO*?Ov>rs70%O{qU87U@ul|q|*+XA07v>0kC zeGx?@2oXY-+AXLT70G-NY0eb(l;A^A&}4{7L7i{2m!J#x_nrUe-20uaA^183>TGpP z6o*geQ^U3Gj9GQEh9@zo(;UrtRe_%i=TjyN=p=bMX+{Zy^h)YBjLGFk z)c}LCH7y3<4mG9fQ0doBL?k}L5)K7IuOpRskQWTo^sGUE8QN^PExba5kMFX#NuA_B z(XuMD7+``{WDUb}*_)+N!b8?1Z4h(k@O2zU^l^Kfnortcm`uS#Hk@2hq)oBu zT~2#@yF-b(Ue|Xp4UNwoh#NtZ5z;v=*06FFXXqz=5}nW0L?m-uJ=BKL(Ht$Dpg(i9 zV7zNcpgDQ5y=$05yoi4aGV=yviMgr}KfYj9KgY#fPge;lUhqkLll8{&y+Q!uUx#y8e^+gqDgsN584JwolK z(BKgo)&*N2IBI}b#z(n%e-w=@@bc_GugY5mF9F`udqvp^?9%@BIk?zm>MhEIRryy@ z2?P^1WYei_9_DC9vFX?tCitRS96i@tn;k7TBocuJvckZ|CBt-}xC8w3TX7p~$aN)c zifEQXE{B{8=8V$&DEd_0#NI6tXAiM3Pq4rula?8HK$J7d#8ANy+&VXt+*B_XCF6GN z>cV-=UR*k_4seK2oSQnurHEY?_?x5VQZu}hZE%uHMi4Ze02I%9m^mNuV3y2Dba^TB_F2tuY;6 My8v(w#^4}-0L$V1wEzGB diff --git a/custom_components/powercalc/analytics/analytics.py b/custom_components/powercalc/analytics/analytics.py index b6a20f6..77cb6fc 100644 --- a/custom_components/powercalc/analytics/analytics.py +++ b/custom_components/powercalc/analytics/analytics.py @@ -12,7 +12,7 @@ import uuid import aiohttp from homeassistant.config_entries import ConfigEntry -from homeassistant.const import __version__ as HA_VERSION # noqa +from homeassistant.const import __version__ as HA_VERSION # noqa: N812 from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -123,7 +123,7 @@ class AnalyticsCollector: counter: Counter[Hashable] = self._data.setdefault(key, Counter()) # type:ignore counter[value] += 1 - def add(self, key: str, value: Any) -> None: # noqa: ANN401 + def add(self, key: str, value: object) -> None: """Add value to listing""" if self._already_seen(key) or value is None: return @@ -194,7 +194,11 @@ class Analytics: cutoff = datetime(2000, 1, 1, tzinfo=UTC) dates = chain( (e.created_at for e in self.hass.config_entries.async_entries(DOMAIN) if e.created_at > cutoff), - (e.created_at for e in entity_registry.async_get(self.hass).entities.values() if e.domain != DOMAIN and e.created_at > cutoff), + ( + e.created_at + for e in entity_registry.async_get(self.hass).entities.values() + if e.domain != DOMAIN and e.created_at > cutoff + ), ) try: diff --git a/custom_components/powercalc/common.py b/custom_components/powercalc/common.py index 20a1c7d..f0c5e7f 100644 --- a/custom_components/powercalc/common.py +++ b/custom_components/powercalc/common.py @@ -46,7 +46,7 @@ def is_number(value: str) -> bool: return math.isfinite(fvalue) -async def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEntity: +def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEntity: """Create object containing all information about the source entity.""" source_entity_domain, source_object_id = split_entity_id(entity_id) @@ -61,7 +61,9 @@ async def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEnt entity_entry = entity_registry.async_get(entity_id) device_registry = dr.async_get(hass) - device_entry = device_registry.async_get(entity_entry.device_id) if entity_entry and entity_entry.device_id else None + device_entry = ( + device_registry.async_get(entity_entry.device_id) if entity_entry and entity_entry.device_id else None + ) unique_id = None supported_color_modes: list[ColorMode] = [] @@ -69,13 +71,14 @@ async def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEnt source_entity_domain = entity_entry.domain unique_id = entity_entry.unique_id if entity_entry.capabilities: - supported_color_modes = entity_entry.capabilities.get( # type: ignore[assignment] + supported_color_modes = entity_entry.capabilities.get( ATTR_SUPPORTED_COLOR_MODES, + [], ) entity_state = hass.states.get(entity_id) if entity_state: - supported_color_modes = entity_state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + supported_color_modes = entity_state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, []) return SourceEntity( source_object_id, @@ -147,13 +150,20 @@ def get_merged_sensor_configuration(*configs: dict, validate: bool = True) -> di CONF_CREATE_ENERGY_SENSORS, ) - is_entity_id_required = not any(key in merged_config for key in (CONF_DAILY_FIXED_ENERGY, CONF_POWER_SENSOR_ID, CONF_MULTI_SWITCH)) + is_entity_id_required = not any( + key in merged_config for key in (CONF_DAILY_FIXED_ENERGY, CONF_POWER_SENSOR_ID, CONF_MULTI_SWITCH) + ) if not is_entity_id_required and CONF_ENTITY_ID not in merged_config: merged_config[CONF_ENTITY_ID] = DUMMY_ENTITY_ID sensor_type = merged_config.get(CONF_SENSOR_TYPE) - if validate and CONF_CREATE_GROUP not in merged_config and CONF_ENTITY_ID not in merged_config and sensor_type != SensorType.GROUP: + if ( + validate + and CONF_CREATE_GROUP not in merged_config + and CONF_ENTITY_ID not in merged_config + and sensor_type != SensorType.GROUP + ): raise SensorConfigurationError( "You must supply an entity_id in the configuration, see the README", ) diff --git a/custom_components/powercalc/config_flow.py b/custom_components/powercalc/config_flow.py index 9e8e9ca..f82de91 100644 --- a/custom_components/powercalc/config_flow.py +++ b/custom_components/powercalc/config_flow.py @@ -4,8 +4,9 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Callable +from inspect import isawaitable import logging -from typing import Any, cast +from typing import Any import uuid from homeassistant.config_entries import ( @@ -21,8 +22,7 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, ) -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -47,7 +47,12 @@ from .const import ( ) from .errors import ModelNotSupportedError, StrategyConfigurationError from .flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults -from .flow_helper.flows.daily_energy import SCHEMA_DAILY_ENERGY_OPTIONS, DailyEnergyConfigFlow, DailyEnergyOptionsFlow, build_daily_energy_config +from .flow_helper.flows.daily_energy import ( + SCHEMA_DAILY_ENERGY_OPTIONS, + DailyEnergyConfigFlow, + DailyEnergyOptionsFlow, + build_daily_energy_config, +) from .flow_helper.flows.global_configuration import ( GlobalConfigurationConfigFlow, GlobalConfigurationOptionsFlow, @@ -65,6 +70,7 @@ from .flow_helper.flows.virtual_power import ( VirtualPowerConfigFlow, VirtualPowerOptionsFlow, ) +from .flow_helper.profile_preview import async_setup_preview as async_setup_powercalc_preview from .flow_helper.schema import ( SCHEMA_ENERGY_SENSOR_TOGGLE, SCHEMA_SENSOR_ENERGY_OPTIONS, @@ -158,9 +164,14 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): super().__init__() + @staticmethod + async def async_setup_preview(hass: HomeAssistant) -> None: + """Set up the config flow preview websocket command.""" + await async_setup_powercalc_preview(hass) + @abstractmethod @callback - def persist_config_entry(self) -> FlowResult: + def persist_config_entry(self) -> ConfigFlowResult: pass # pragma: no cover def _async_step(self, step: Step) -> Callable: @@ -174,12 +185,12 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): return _async_step - async def async_step(self, step: Step, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step(self, step: Step, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle a config flow step by delegating to specific handler.""" step_method = f"async_step_{step}" for handler in self.flow_handlers.values(): if hasattr(handler, step_method): - return await getattr(handler, step_method)(user_input) # type:ignore + return await getattr(handler, step_method)(user_input) # type: ignore[no-any-return] raise SchemaFlowError("No handler defined") # pragma: nocover async def validate_strategy_config(self, user_input: dict[str, Any] | None = None) -> None: @@ -188,8 +199,14 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): self.sensor_config.get(CONF_MODE) or self.selected_profile.calculation_strategy, # type: ignore ) factory = PowerCalculatorStrategyFactory(self.hass) + assert self.source_entity is not None try: - await factory.create(user_input or self.sensor_config, strategy_name, self.selected_profile, self.source_entity) # type: ignore + await factory.create( + user_input or self.sensor_config, + strategy_name, + self.selected_profile, + self.source_entity, + ) except StrategyConfigurationError as error: _LOGGER.error(str(error)) raise SchemaFlowError(error.get_config_flow_translate_key() or "unknown") from error @@ -227,12 +244,13 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): self, form_step: PowercalcFormStep, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the current step.""" if user_input is not None: if form_step.validate_user_input is not None: try: - user_input = await form_step.validate_user_input(user_input) + validated_input = form_step.validate_user_input(user_input) + user_input = await validated_input if isawaitable(validated_input) else validated_input except SchemaFlowError as exc: return await self._show_form(form_step, exc) @@ -243,14 +261,15 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): self.handled_steps.append(form_step.step) next_step = form_step.next_step if callable(form_step.next_step): - next_step = await form_step.next_step(user_input) + resolved_next_step = form_step.next_step(user_input) + next_step = await resolved_next_step if isawaitable(resolved_next_step) else resolved_next_step if not next_step: return await self.handle_final_steps( skip_advanced=not form_step.continue_advanced_step, skip_utility_meter_options=not form_step.continue_utility_meter_options_step, ) - return await getattr(self, f"async_step_{next_step}")() # type: ignore + return await getattr(self, f"async_step_{next_step}")() # type: ignore[no-any-return] return await self._show_form(form_step) @@ -258,15 +277,15 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): self, skip_advanced: bool = False, skip_utility_meter_options: bool = False, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the final steps of the flow if needed and persist the config entry.""" if not skip_advanced and self.selected_sensor_type == SensorType.VIRTUAL_POWER: - return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_power_advanced() # type:ignore + return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_power_advanced() # type: ignore[no-any-return] if not skip_utility_meter_options and self.sensor_config.get(CONF_CREATE_UTILITY_METERS): return await self.async_step_utility_meter_options() return self.persist_config_entry() - async def _show_form(self, form_step: PowercalcFormStep, error: SchemaFlowError | None = None) -> FlowResult: + async def _show_form(self, form_step: PowercalcFormStep, error: SchemaFlowError | None = None) -> ConfigFlowResult: # Show form for next step last_step = None if not callable(form_step.next_step): @@ -288,9 +307,12 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): async def _get_schema(self, form_step: PowercalcFormStep) -> vol.Schema: if isinstance(form_step.schema, vol.Schema): return form_step.schema - return await form_step.schema() + schema = await form_step.schema() + if schema is None: + return vol.Schema({}) + return schema - async def async_step_utility_meter_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_utility_meter_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for utility meter options.""" return await self.handle_form_step( PowercalcFormStep( @@ -300,7 +322,7 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): user_input, ) - async def async_step_energy_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_energy_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for utility meter options.""" return await self.handle_form_step( PowercalcFormStep( @@ -315,7 +337,7 @@ class PowercalcCommonFlow(ABC, ConfigEntryBaseFlow): class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN): """Handle a config flow for PowerCalc.""" - VERSION = 7 + VERSION = 8 def __init__(self) -> None: """Initialize options flow.""" @@ -367,13 +389,13 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN): self.is_library_flow = True if discovery_info.get(CONF_MODE) == CalculationStrategy.WLED: - return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_wled() # type:ignore + return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_wled() # type: ignore[no-any-return] if len(power_profiles) > 1: - return cast(ConfigFlowResult, await self.flow_handlers[FlowType.LIBRARY].async_step_library_multi_profile()) + return await self.flow_handlers[FlowType.LIBRARY].async_step_library_multi_profile() # type: ignore[no-any-return] self.selected_profile = power_profiles[0] - return cast(ConfigFlowResult, await self.flow_handlers[FlowType.LIBRARY].async_step_library()) + return await self.flow_handlers[FlowType.LIBRARY].async_step_library() # type: ignore[no-any-return] async def async_step_user( self, @@ -394,15 +416,15 @@ class PowercalcConfigFlow(PowercalcCommonFlow, ConfigFlow, domain=DOMAIN): return self.async_show_menu(step_id=Step.USER, menu_options=menu) - async def async_step_menu_library(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_menu_library(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the Virtual power (library) step. We forward to the virtual_power step, but without the strategy selector displayed. """ self.is_library_flow = True - return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_virtual_power(user_input) # type:ignore + return await self.flow_handlers[FlowType.VIRTUAL_POWER].async_step_virtual_power(user_input) # type: ignore[no-any-return] @callback - def persist_config_entry(self) -> FlowResult: + def persist_config_entry(self) -> ConfigFlowResult: """Create the config entry.""" self.sensor_config.update({CONF_SENSOR_TYPE: self.selected_sensor_type}) self.sensor_config.update({CONF_NAME: self.name}) @@ -442,18 +464,21 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" self.selected_sensor_type = self.sensor_config.get(CONF_SENSOR_TYPE) or SensorType.VIRTUAL_POWER self.source_entity_id = self.sensor_config.get(CONF_ENTITY_ID) if self.config_entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID: self.global_config = get_global_powercalc_config(self) - return self.async_show_menu(step_id=Step.INIT, menu_options=self.flow_handlers[FlowType.GLOBAL_CONFIGURATION].build_global_config_menu()) + return self.async_show_menu( + step_id=Step.INIT, + menu_options=self.flow_handlers[FlowType.GLOBAL_CONFIGURATION].build_global_config_menu(), + ) self.sensor_config = dict(self.config_entry.data) if self.source_entity_id: - self.source_entity = await create_source_entity( + self.source_entity = create_source_entity( self.source_entity_id, self.hass, ) @@ -463,7 +488,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): return self.async_show_menu(step_id=Step.INIT, menu_options=self.build_menu()) - async def initialize_library_profile(self) -> FlowResult | None: + async def initialize_library_profile(self) -> ConfigFlowResult | None: """Initialize the library profile, when manufacturer and model are set.""" manufacturer: str | None = self.sensor_config.get(CONF_MANUFACTURER) model: str | None = self.sensor_config.get(CONF_MODEL) @@ -519,19 +544,29 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): return True - async def async_step_basic_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_basic_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_options_step(user_input, self.build_basic_options_schema(), Step.BASIC_OPTIONS) - async def async_step_advanced_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_advanced_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_options_step(user_input, SCHEMA_POWER_ADVANCED, Step.ADVANCED_OPTIONS) - async def async_step_utility_meter_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_utility_meter_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" - return await self.async_handle_options_step(user_input, SCHEMA_UTILITY_METER_OPTIONS, Step.UTILITY_METER_OPTIONS) + return await self.async_handle_options_step( + user_input, + SCHEMA_UTILITY_METER_OPTIONS, + Step.UTILITY_METER_OPTIONS, + ) - async def async_handle_options_step(self, user_input: dict[str, Any] | None, schema: vol.Schema, step: Step) -> FlowResult: + async def async_handle_options_step( + self, + user_input: dict[str, Any] | None, + schema: vol.Schema, + step: Step, + form_kwarg: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """ Generic handler for all the option steps. processes user input against the select schema. @@ -543,11 +578,13 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): errors = await self.process_all_options(user_input, schema) if not errors: return self.persist_config_entry() - return self.async_show_form(step_id=step, data_schema=schema, errors=errors) + return self.async_show_form(step_id=step, data_schema=schema, errors=errors, **(form_kwarg or {})) - def persist_config_entry(self) -> FlowResult: + def persist_config_entry(self) -> ConfigFlowResult: """Persist changed options on the config entry.""" - data = (self.config_entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID and self.global_config) or self.sensor_config + data = ( + self.config_entry.unique_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID and self.global_config + ) or self.sensor_config self.hass.config_entries.async_update_entry( self.config_entry, @@ -597,7 +634,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): ) -> None: """ Process the provided user input against the schema. - Update the current_config dictionary with the new options. We use that to save the data to config entry later on. + Update current_config with the new options. Used to save the data to the config entry later. """ for key in schema.schema: if isinstance(key, vol.Marker): @@ -632,7 +669,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow): {vol.Optional(CONF_STANDBY_POWER): vol.Coerce(float)}, ) - return schema.extend( # type: ignore + return schema.extend( # type: ignore[no-any-return] { **SCHEMA_ENERGY_SENSOR_TOGGLE.schema, **SCHEMA_UTILITY_METER_TOGGLE.schema, diff --git a/custom_components/powercalc/configuration/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/configuration/__pycache__/__init__.cpython-314.pyc index e63851f0d6d3bf2aa589ef35b74a7f881b82afd9..9413c88d450ad271d6b7cc6f41ea6d0fb71fd3ab 100644 GIT binary patch delta 19 ZcmZ3$xPXycn~#@^0SHdN(4WXX6#ysL1hD`B delta 19 ZcmZ3$xPXycn~#@^0SI!IsZZpd3IHTD1PA~C diff --git a/custom_components/powercalc/configuration/__pycache__/global_config.cpython-314.pyc b/custom_components/powercalc/configuration/__pycache__/global_config.cpython-314.pyc index 4f25d1d1ea5242445da19a9d85bdac623b5b2647..094b10f219a28d343358ec2f9a23441f69b5b858 100644 GIT binary patch delta 124 zcmcbt`9OnLn~#@^0SHdN(9fE`kvExLV2SMq20cc}Jz8MujMU@?c4-b~ppwrf%nX~C zvx_rNKF1}&A?MW(Ec& z2Kmj4*~OWC+!=zHvw#8>j19~_tXvH83^BcW_n8Q2+oq=1;i zlsS`8lV$QPE)7P%%}m@U8CiERGBCW{e2%A!F^)@Pg~*QV%UoX9S-dW=c-`UBTTybr z`!ZL+b(Vk&ECCO=B`&Z?d}L=}^=7=u67aKF3us-FtDc(y<55EeAbCuX(M^Ztn2<1t Yt;*=8$8t=K9mLjS1d^M}_+GIB01qiX<^TWy diff --git a/custom_components/powercalc/configuration/global_config.py b/custom_components/powercalc/configuration/global_config.py index ef6d0aa..c9293a0 100644 --- a/custom_components/powercalc/configuration/global_config.py +++ b/custom_components/powercalc/configuration/global_config.py @@ -53,7 +53,7 @@ FLAG_HAS_GLOBAL_GUI_CONFIG = "has_global_gui_config" _LOGGER = logging.getLogger(__name__) -async def get_global_configuration(hass: HomeAssistant, config: ConfigType) -> ConfigType: +def get_global_configuration(hass: HomeAssistant, config: ConfigType) -> ConfigType: # Default configuration values default_config = { CONF_ENABLE_ANALYTICS: False, @@ -96,8 +96,8 @@ async def get_global_configuration(hass: HomeAssistant, config: ConfigType) -> C if yaml_config: global_config.update(yaml_config) - await handle_legacy_discovery_config(hass, global_config, yaml_config) - await handle_legacy_update_interval_config(hass, global_config, yaml_config) + handle_legacy_discovery_config(hass, global_config, yaml_config) + handle_legacy_update_interval_config(hass, global_config, yaml_config) return global_config diff --git a/custom_components/powercalc/const.py b/custom_components/powercalc/const.py index 7a2a879..7dbdec4 100644 --- a/custom_components/powercalc/const.py +++ b/custom_components/powercalc/const.py @@ -17,7 +17,7 @@ from homeassistant.const import ( EntityCategory, ) -MIN_HA_VERSION = "2025.1" +MIN_HA_VERSION = "2026.1" BUILT_IN_LIBRARY_DIR = "powercalc_profiles" @@ -170,6 +170,11 @@ CONF_UTILITY_METER_TARIFFS = "utility_meter_tariffs" CONF_UTILITY_METER_TYPES = "utility_meter_types" CONF_VALUE = "value" CONF_VALUE_TEMPLATE = "value_template" + +# Wrapper keys for ChooseSelector config flow forms (not stored in config entries). +CONF_FIXED_VALUE = "fixed_value" +CONF_DAILY_ENERGY_VALUE = "daily_energy_value" +CONF_PLAYBOOK_ID = "playbook_id" CONF_VARIABLES = "variables" CONF_VOLTAGE = "voltage" CONF_WILDCARD = "wildcard" @@ -198,7 +203,6 @@ class UnitPrefix(StrEnum): ENTITY_CATEGORIES = [ - EntityCategory.CONFIG, EntityCategory.DIAGNOSTIC, None, ] @@ -225,10 +229,12 @@ API_URL = "https://api.powercalc.nl" MANUFACTURER_WLED = "WLED" ATTR_CALCULATION_MODE = "calculation_mode" +ATTR_MEMBERS = "members" ATTR_ENERGY_SENSOR_ENTITY_ID = "energy_sensor_entity_id" ATTR_ENTITIES = "entities" ATTR_INTEGRATION = "integration" ATTR_IS_GROUP = "is_group" +ATTR_STATE = "state" ATTR_SOURCE_ENTITY = "source_entity" ATTR_SOURCE_DOMAIN = "source_domain" @@ -236,6 +242,7 @@ SERVICE_ACTIVATE_PLAYBOOK = "activate_playbook" SERVICE_CALIBRATE_UTILITY_METER = "calibrate_utility_meter" SERVICE_CALIBRATE_ENERGY = "calibrate_energy" SERVICE_CHANGE_GUI_CONFIGURATION = "change_gui_config" +SERVICE_DEBUG_GROUP = "debug_group" SERVICE_GET_ACTIVE_PLAYBOOK = "get_active_playbook" SERVICE_GET_GROUP_ENTITIES = "get_group_entities" SERVICE_INCREASE_DAILY_ENERGY = "increase_daily_energy" diff --git a/custom_components/powercalc/device_binding.py b/custom_components/powercalc/device_binding.py index 038e24e..d41e371 100644 --- a/custom_components/powercalc/device_binding.py +++ b/custom_components/powercalc/device_binding.py @@ -4,7 +4,7 @@ from awesomeversion import AwesomeVersion from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE, - __version__ as HA_VERSION, # noqa + __version__ as HA_VERSION, # noqa: N812 ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry @@ -90,7 +90,11 @@ def remove_stale_devices( ) -def get_device_info(hass: HomeAssistant, sensor_config: ConfigType, source_entity: SourceEntity | None) -> DeviceInfo | None: +def get_device_info( + hass: HomeAssistant, + sensor_config: ConfigType, + source_entity: SourceEntity | None, +) -> DeviceInfo | None: """ Get device info for a given powercalc entity configuration. Prefer user configured device, when it is not set fallback to the same device as the source entity diff --git a/custom_components/powercalc/diagnostics.py b/custom_components/powercalc/diagnostics.py index f395807..d670464 100644 --- a/custom_components/powercalc/diagnostics.py +++ b/custom_components/powercalc/diagnostics.py @@ -20,7 +20,7 @@ async def async_get_config_entry_diagnostics( data: dict = { "entry": entry.as_dict(), - "config_entry_count_per_type": await get_count_by_sensor_type(hass), + "config_entry_count_per_type": get_count_by_sensor_type(hass), "yaml_config": await get_yaml_configuration(hass), } @@ -31,7 +31,7 @@ async def async_get_config_entry_diagnostics( return data -async def get_count_by_sensor_type(hass: HomeAssistant) -> dict[SensorType, int]: +def get_count_by_sensor_type(hass: HomeAssistant) -> dict[SensorType, int]: count_per_type = {} entries = get_entries_excluding_global_config(hass) for e in entries: diff --git a/custom_components/powercalc/discovery.py b/custom_components/powercalc/discovery.py index 42d549f..ba0698e 100644 --- a/custom_components/powercalc/discovery.py +++ b/custom_components/powercalc/discovery.py @@ -1,17 +1,17 @@ from __future__ import annotations from collections.abc import Awaitable, Callable -from datetime import timedelta +from datetime import datetime, timedelta from enum import StrEnum import logging import re -from typing import Any +from typing import Any, TypeVar from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, SOURCE_USER, ConfigEntry from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_UNIQUE_ID -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import discovery_flow import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import EntityCategory @@ -33,13 +33,22 @@ from .const import ( MANUFACTURER_WLED, CalculationStrategy, ) -from .group_include.filter import CategoryFilter, CompositeFilter, DomainFilter, FilterOperator, LambdaFilter, NotFilter, get_filtered_entity_list +from .group_include.filter import ( + CategoryFilter, + CompositeFilter, + DomainFilter, + FilterOperator, + LambdaFilter, + NotFilter, + get_filtered_entity_list, +) from .helpers import get_or_create_unique_id from .power_profile.factory import get_power_profile from .power_profile.library import ModelInfo, ProfileLibrary from .power_profile.power_profile import SUPPORTED_DOMAINS, DeviceType, DiscoveryBy, PowerProfile _LOGGER = logging.getLogger(__name__) +_DiscoverySourceT = TypeVar("_DiscoverySourceT", er.RegistryEntry, dr.DeviceEntry) async def get_power_profile_by_source_entity(hass: HomeAssistant, source_entity: SourceEntity) -> PowerProfile | None: @@ -102,6 +111,7 @@ class DiscoveryManager: self.library: ProfileLibrary | None = None self._exclude_device_types = exclude_device_types or [] self._exclude_self_usage_profiles = exclude_self_usage_profiles or False + self._cancel_rediscover_interval: CALLBACK_TYPE | None = None self._status = DiscoveryStatus.NOT_STARTED if enabled else DiscoveryStatus.DISABLED async def setup(self) -> None: @@ -112,14 +122,18 @@ class DiscoveryManager: await self.start_discovery() - async def _rediscover(_: Any) -> None: # noqa: ANN401 + async def _rediscover(_: datetime) -> None: """Rediscover entities.""" await self.update_library_and_rediscover() - async_track_time_interval( + if self._cancel_rediscover_interval: # pragma: no cover + self._cancel_rediscover_interval() + + self._cancel_rediscover_interval = async_track_time_interval( self.hass, _rediscover, timedelta(hours=2), + cancel_on_shutdown=True, ) async def update_library_and_rediscover(self) -> None: @@ -130,6 +144,9 @@ class DiscoveryManager: async def start_discovery(self) -> None: """Start the discovery procedure.""" + if self._status == DiscoveryStatus.DISABLED: + _LOGGER.debug("Discovery manager is disabled, skipping discovery run") + return if self._status == DiscoveryStatus.IN_PROGRESS: _LOGGER.debug("Discovery already in progress, skipping new discovery run") return @@ -139,10 +156,10 @@ class DiscoveryManager: _LOGGER.debug("Start auto discovery") _LOGGER.debug("Start entity discovery") - await self.perform_discovery(self.get_entities, self.create_entity_source, DiscoveryBy.ENTITY) # type: ignore[arg-type] + await self.perform_discovery(self.get_entities, self.create_entity_source, DiscoveryBy.ENTITY) _LOGGER.debug("Start device discovery") - await self.perform_discovery(self.get_devices, self.create_device_source, DiscoveryBy.DEVICE) # type: ignore[arg-type] + await self.perform_discovery(self.get_devices, self.create_device_source, DiscoveryBy.DEVICE) _LOGGER.debug("Done auto discovery") self._status = DiscoveryStatus.FINISHED @@ -158,7 +175,7 @@ class DiscoveryManager: if not entity_id or entity_id == DUMMY_ENTITY_ID: continue - entity = await create_source_entity(str(entity_id), self.hass) + entity = create_source_entity(str(entity_id), self.hass) if entity and entity.device_entry: self.initialized_flows.add(f"pc_{entity.device_entry.id}") self.initialized_flows.add(entity_id) @@ -173,13 +190,13 @@ class DiscoveryManager: async def perform_discovery( self, - source_provider: Callable[[], Awaitable[list]], - source_creator: Callable[[er.RegistryEntry | dr.DeviceEntry], Awaitable[SourceEntity]], + source_provider: Callable[[], Awaitable[list[_DiscoverySourceT]]], + source_creator: Callable[[_DiscoverySourceT], Awaitable[SourceEntity]], discovery_type: DiscoveryBy, ) -> None: """Generalized discovery procedure for entities and devices.""" for source in await source_provider(): - log_identifier = source.entity_id if discovery_type == DiscoveryBy.ENTITY else source.id + log_identifier = str(getattr(source, "entity_id", getattr(source, "id", "unknown"))) try: model_info = await self.extract_model_info_from_device_info(source) if not model_info: @@ -229,7 +246,7 @@ class DiscoveryManager: async def create_entity_source(self, entity_entry: er.RegistryEntry) -> SourceEntity: """Create SourceEntity for an entity.""" - return await create_source_entity(entity_entry.entity_id, self.hass) + return create_source_entity(entity_entry.entity_id, self.hass) @staticmethod async def create_device_source(device_entry: dr.DeviceEntry) -> SourceEntity: @@ -267,13 +284,19 @@ class DiscoveryManager: power_profiles = [] for model_info in models: - profile = await get_power_profile(self.hass, {}, source_entity, model_info=model_info, process_variables=False) + profile = await get_power_profile( + self.hass, + {}, + source_entity, + model_info=model_info, + process_variables=False, + ) if not profile or profile.discovery_by != discovery_type: # pragma: no cover continue - if discovery_type == DiscoveryBy.ENTITY and not profile.is_entity_domain_supported( - source_entity.entity_entry, # type: ignore[arg-type] - ): - continue + if discovery_type == DiscoveryBy.ENTITY: + entity_entry = source_entity.entity_entry + if entity_entry is not None and not profile.is_entity_domain_supported(entity_entry): + continue if profile.device_type in self._exclude_device_types: continue if self._exclude_self_usage_profiles and profile.only_self_usage: @@ -294,7 +317,11 @@ class DiscoveryManager: """Initialize the discovery flow for a WLED light.""" if DeviceType.LIGHT in self._exclude_device_types: return - unique_id = f"pc_{source_entity.device_entry.id}" if source_entity.device_entry else get_or_create_unique_id({}, source_entity, None) + unique_id = ( + f"pc_{source_entity.device_entry.id}" + if source_entity.device_entry + else get_or_create_unique_id({}, source_entity, None) + ) if self._is_already_discovered(source_entity, unique_id): _LOGGER.debug( "%s: Already setup with discovery, skipping new discovery (unique_id=%s)", @@ -352,9 +379,9 @@ class DiscoveryManager: ], FilterOperator.OR, ) - return await get_filtered_entity_list(self.hass, NotFilter(entity_filter)) + return get_filtered_entity_list(self.hass, NotFilter(entity_filter)) - async def get_devices(self) -> list: + async def get_devices(self) -> list[dr.DeviceEntry]: """Fetch device entries.""" return list(dr.async_get(self.hass).devices.values()) @@ -364,6 +391,9 @@ class DiscoveryManager: async def disable(self) -> None: """Disable the discovery.""" + if self._cancel_rediscover_interval: + self._cancel_rediscover_interval() + self._cancel_rediscover_interval = None self._status = DiscoveryStatus.DISABLED self.initialized_flows = set() flows = self.hass.config_entries.flow.async_progress_by_handler(DOMAIN) @@ -423,7 +453,9 @@ class DiscoveryManager: # see https://github.com/home-assistant/core/pull/166187 manufacturer = str(device_entry.manufacturer).strip() model = str(device_entry.model).strip() - model_id = str(device_entry.model_id).strip() if hasattr(device_entry, "model_id") and device_entry.model_id else None + model_id = ( + str(device_entry.model_id).strip() if hasattr(device_entry, "model_id") and device_entry.model_id else None + ) if len(manufacturer) == 0 or len(model) == 0: return None @@ -508,7 +540,9 @@ class DiscoveryManager: # Find entity ids in yaml config (Legacy) if SENSOR_DOMAIN in self.ha_config: # pragma: no cover sensor_config = self.ha_config.get(SENSOR_DOMAIN) - platform_entries = [item for item in sensor_config or {} if isinstance(item, dict) and item.get(CONF_PLATFORM) == DOMAIN] + platform_entries = [ + item for item in sensor_config or {} if isinstance(item, dict) and item.get(CONF_PLATFORM) == DOMAIN + ] for entry in platform_entries: entities.extend(self._find_entity_ids_in_yaml_config(entry)) @@ -521,7 +555,11 @@ class DiscoveryManager: # Add entities from existing config entries entities.extend( - [str(entry.data.get(CONF_ENTITY_ID)) for entry in self.hass.config_entries.async_entries(DOMAIN) if entry.source == SOURCE_USER], + [ + str(entry.data.get(CONF_ENTITY_ID)) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.source == SOURCE_USER + ], ) return entities diff --git a/custom_components/powercalc/filter/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/filter/__pycache__/__init__.cpython-314.pyc index 67097b7a11dd53f4326df3ec56c3ca27b6ded28b..f1baac5912484c09e3d8cab713ff7678bd4036b3 100644 GIT binary patch delta 19 ZcmbQqIFpfEn~#@^0SHdN(4WZN2LLA31e^c> delta 19 ZcmbQqIFpfEn~#@^0SI!IsZZqY0{|m21M>g? diff --git a/custom_components/powercalc/filter/__pycache__/outlier.cpython-314.pyc b/custom_components/powercalc/filter/__pycache__/outlier.cpython-314.pyc index 603824f7f3c56e935bf474a84e6a256b85a40d03..36accefc62eddf4f58d0e270805d240a35525026 100644 GIT binary patch delta 20 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 BV&wn; diff --git a/custom_components/powercalc/flow_helper/__pycache__/profile_preview.cpython-314.pyc b/custom_components/powercalc/flow_helper/__pycache__/profile_preview.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51e93ac52eee33c06a51b5c98a6dfb331c37da87 GIT binary patch literal 11278 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 literal 0 HcmV?d00001 diff --git a/custom_components/powercalc/flow_helper/__pycache__/schema.cpython-314.pyc b/custom_components/powercalc/flow_helper/__pycache__/schema.cpython-314.pyc index 14ba53287fd72766bdd9a16852dac3e531373479..a236e0647d8ca1569a0b1aa0f763fb1a4b8cb597 100644 GIT binary patch delta 20 acmcbnaZQ6;n~#@^0SHdN(BH`IEdl^K2?bRE delta 20 acmcbnaZQ6;n~#@^0SI!Isc+==76AY?yacQO diff --git a/custom_components/powercalc/flow_helper/common.py b/custom_components/powercalc/flow_helper/common.py index a73a277..e325faf 100644 --- a/custom_components/powercalc/flow_helper/common.py +++ b/custom_components/powercalc/flow_helper/common.py @@ -1,4 +1,4 @@ -from collections.abc import Callable, Coroutine +from collections.abc import Awaitable, Callable, Coroutine import copy from dataclasses import dataclass from enum import StrEnum @@ -59,6 +59,9 @@ class FlowType(StrEnum): GLOBAL_CONFIGURATION = "global_configuration" +type MaybeAwaitable[R] = R | Awaitable[R] + + @dataclass(slots=True) class PowercalcFormStep: schema: vol.Schema | Callable[[], Coroutine[Any, Any, vol.Schema | None]] @@ -66,12 +69,12 @@ class PowercalcFormStep: validate_user_input: ( Callable[ [dict[str, Any]], - Coroutine[Any, Any, dict[str, Any]], + MaybeAwaitable[dict[str, Any]], ] | None ) = None - next_step: Step | Callable[[dict[str, Any]], Coroutine[Any, Any, Step | None]] | None = None + next_step: Step | Callable[[dict[str, Any]], MaybeAwaitable[Step | None]] | None = None continue_utility_meter_options_step: bool = False continue_advanced_step: bool = False form_kwarg: dict[str, Any] | None = None @@ -88,9 +91,85 @@ def fill_schema_defaults( new_key = key if key in options and isinstance(key, vol.Marker): if isinstance(key, vol.Optional) and callable(key.default) and key.default(): - new_key = vol.Optional(key.schema, default=options.get(key)) # type: ignore + new_key = vol.Optional(key.schema, default=options.get(key)) # type: ignore[call-overload] + elif isinstance(key, vol.Required): + new_key = vol.Required(key.schema, default=options.get(key)) # type: ignore[call-overload] + new_key.description = {"suggested_value": options.get(key)} # type: ignore[call-overload] elif "suggested_value" not in (new_key.description or {}): new_key = copy.copy(key) - new_key.description = {"suggested_value": options.get(key)} # type: ignore + new_key.description = {"suggested_value": options.get(key)} # type: ignore[call-overload] schema[new_key] = val return vol.Schema(schema) + + +def unwrap_choose_selector( + user_input: dict[str, Any], + wrapper_key: str, + value_key: str | Callable[[object], str] | None = None, +) -> dict[str, Any]: + """ + Unwrap a ChooseSelector value in user_input back into flat keys. + + A ChooseSelector value looks like {"active_choice": "", "": }. + The wrapper key is dropped, and the active choice's value is merged back into user_input. + Home Assistant schema validation returns the selected value directly; use ``value_key`` + to map that validated value back to a config key. + """ + if wrapper_key not in user_input: + return user_input + + raw = user_input.pop(wrapper_key) + if not isinstance(raw, dict): + if isinstance(value_key, str): + user_input[value_key] = raw + elif value_key is not None: + user_input[value_key(raw)] = raw + return user_input + + if "active_choice" not in raw: + user_input.update(raw) + return user_input + + active = raw["active_choice"] + value = raw.get(active) + if value is None: + return user_input + + if isinstance(value, dict): + user_input.update(value) + else: + user_input[active] = value + return user_input + + +def wrap_choose_selector( + form_data: dict[str, Any], + wrapper_key: str, + choices: dict[str, list[str] | str], + *, + raw_value: bool = False, +) -> dict[str, Any]: + """ + Build the ChooseSelector value for ``wrapper_key`` from existing flat ``form_data``. + + ``choices`` maps the choice id to either a single key (the value of that key becomes + the choice value) or a list of keys (a dict of those keys becomes the choice value). + The first choice that has a matching key in form_data is used. + """ + for choice_id, mapping in choices.items(): + keys = [mapping] if isinstance(mapping, str) else mapping + present = {key: form_data[key] for key in keys if key in form_data} + if not present: + continue + + if isinstance(mapping, str): + choice_value: Any = present[mapping] + else: + choice_value = present + + if raw_value: + return {**form_data, wrapper_key: choice_value} + + return {**form_data, wrapper_key: {"active_choice": choice_id, choice_id: choice_value}} + + return form_data diff --git a/custom_components/powercalc/flow_helper/dynamic_field_builder.py b/custom_components/powercalc/flow_helper/dynamic_field_builder.py index 3900f9c..8e107d7 100644 --- a/custom_components/powercalc/flow_helper/dynamic_field_builder.py +++ b/custom_components/powercalc/flow_helper/dynamic_field_builder.py @@ -7,7 +7,11 @@ from custom_components.powercalc.common import SourceEntity from custom_components.powercalc.power_profile.power_profile import PowerProfile -def build_dynamic_field_schema(hass: HomeAssistant, profile: PowerProfile, source_entity: SourceEntity | None) -> vol.Schema: +def build_dynamic_field_schema( + hass: HomeAssistant, + profile: PowerProfile, + source_entity: SourceEntity | None, +) -> vol.Schema: schema = {} for field in profile.custom_fields: field_description = field.description @@ -22,7 +26,8 @@ def build_dynamic_field_schema(hass: HomeAssistant, profile: PowerProfile, sourc if "entity" in field.selector and source_entity and source_entity.device_entry: entity_reg = er.async_get(hass) field.selector["entity"]["include_entities"] = [ - entity.entity_id for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id) + entity.entity_id + for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id) ] schema[key] = selector(field.selector) diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/__init__.cpython-314.pyc index c1b03bfb912fc17a863fd166e748abce06ff355c..72cd1f8b592756af8d0456abe35476e496694298 100644 GIT binary patch delta 19 ZcmZ3&xP*~gn~#@^0SHdN(4WXX696b|1ib(N delta 19 ZcmZ3&xP*~gn~#@^0SI!IsZZpd2>>L>1QY-O diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/daily_energy.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/daily_energy.cpython-314.pyc index b1a55cbcf4f878d1a1e949a212821505db17524e..a711c7f69a949f31e37fd7a7b510b2a831d3249d 100644 GIT binary patch literal 8719 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 delta 5277 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 NHRsm92~^?p{ugU$Oq2is diff --git a/custom_components/powercalc/strategy/__pycache__/lut.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/lut.cpython-314.pyc index f0646e4349b1ee8b5a20ca56168e9d1ed763bd74..5671f51e27f85e5abaddb14b3da13c3a3dc1484d 100644 GIT binary patch delta 2447 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 diff --git a/custom_components/powercalc/strategy/__pycache__/multi_switch.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/multi_switch.cpython-314.pyc index 77a03cbbdd2238a016e2fb2c4f496418a965eb8b..18da86857b88f46aac9ea7512e68f7f89c1470cb 100644 GIT binary patch delta 344 zcmdm^zE7Q3n~#@^0SHdN(9a6k$eYh*CdeAj_>lobu5kSTrZ?n4_(e)Ug~?1HaO+SK!J6x{t z$bR6OEXG@?AP?mKEEWe^+#>73$+*zSg@xsyh!}{}a*}u9oVg@+HZDH3)+>+~8N6AaY$$^`fBa1%9;) w9BPx_@#Qf(O^)T?1|&rUPIH37=Bpae#LX`RjxY(RGHP9rvi`;Zq`@Hs0M%7jZ2$lO delta 385 zcmdm|zDJ!`n~#@^0SI!Isb~3atnjGug+O_kM&fuT@oGBbNUqs!)I_Roxrrkl$-V}R7;D_nmWZ6;sh zK5zLFsPX4Zh6aWk-2B(MWiE2dT;f*wz{23bez6pH|f28QnpjI1gjLBs`h2QUrs9V1W{20(0Pknf~`j0T3V$p_d(Cl~Q0iG$7I z0}I~Z7rxG~e34)I0*CVC4}5uyj+5j0w*g5pfzzCzK>Vr#v}5xtfg?-;DvVkeq^!R& I0BNvg0PLY#*8l(j diff --git a/custom_components/powercalc/strategy/__pycache__/playbook.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/playbook.cpython-314.pyc index b9204485bace3a869f92417ce808e8d031eda9c1..5a2b78f334540f8ea02da6a5ea5353be6434898a 100644 GIT binary patch literal 18986 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 C=^tzW diff --git a/custom_components/powercalc/strategy/__pycache__/selector.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/selector.cpython-314.pyc index c9643f7d774251bdfaa4db7255943dd380c3e4cc..b0dfbb39526cd5c282201ac7bae1d56dd2b2cc14 100644 GIT binary patch delta 20 acmeyu_l1vJn~#@^0SHdN(BH_N$qoQJ1O;vY delta 20 acmeyu_l1vJn~#@^0SI!Isc+=YWCs8?w*pZxbzG zE7D5L2-t}_!CVY%HMvPvP?Kyy_#|j-R9tC}z`+-S>$44W))rvWM`LVVJPp6Q8^*GY zu;K=dvNP}8FWBJRpb9L}5Ws`b1*0^-STyyAde$l%6m6vi8ifxxwV%dtZxcc@K#09* zNXc;+l|)t%)2ziel;8wJ+J>9D0BnrD4_VCkPHE_eb|7@JQDrzqgE-#HQIrLwP9e?~ z#6fmL>W|`n+6qvQ@TqiBL!P^~GfNn&7g)lax1CAbp%B?PEtVF>jm3%y!RPmMjd9ut z%Ve8~Qfv$1ed;X#QXV^Ix%S|=fsn=@GGUvFk3Ex*{e|s@JQm}IG55>lb2$3gPvek~ z%;7p|*Eelh=AQi8*eeB7^HIsS{E-&0?#9_8HO}(ta0Df>CGK%O+zkGnt7lJgf2h-L z9nb-s?xjg5)BQa)@m(d!?)Wn?&OnFbyQ;tPpN@3mO3ibV=b6jnhO3ns(4kO*+Mfnt z9wcgAFgdwXjrjpyjd?C54gU)_&Hw>L6Qpax7RUETuWWjQ>o>N7eB>!8G6-PlT+Wz- zIx(p3kt21oJ(%^fwp0B>K%kxn7`sij;S_!g z*d4ltCLn4yr?%^V*@1CzGb_Z)+$r9~zS~9HrZW8ekHqw_1vv=?#1CCfC862ChKi_<0Q z!&PO*yTGW#&IH?Jo*aeU3MR)?_|2{H06gP(nYn6a(VD56#u+*SQ*2)wyJ53I5QH_d Z{|o6|BOPDK(KRx(Ew>B(KL`Ln{|oEUJ01W4 delta 1091 zcma)*&rcIU6vt=U{n6d-mj0506_?IyF!0rfyU zagyNt4dH1LXj~j@RXpAQ{90)hxoAM(jB(+JuyYJ1+`^q24*-r; zi+@mtF97K3TpaDXD#!-;+7a8-SQ0!Ghguv+`_ePVske2L;(IE(IacQ{06h=QMe*=l zS%3~Yl7RUXo*EYzJkj&ZVuKjAGneMmT0ii9=>zw^4Xrcw3 z5UnDuR9%S;D?G0XKAf& l-(hzl#P83=!~GqklV|X|t1H(&(28Iu83<5IO`Bg9StF92au4#Qy)B3um z>qSl1EBvk>nHhK`9|($0mzpSbT~O(wpwe|gwTpsk3reo5TV7PRysqwiQQi5npvwn# z22PO=JPbVIliPTI%J9q1NWILja)CqTXE86({w61OcQ(dDTrM{5Y?IgWWy%Bs1&f7& zL<7Tj20m7I56=UY)XQDTOiK;_5)t+Zetx@vvHbl>r9OS395;Ullg* X5&y;{;?Bqz!gxX01!0f}y#h}c<&fvqc`7F01V|^AQ1H(&(28J7I zM%UGBE~?oaV7cxPbkQN`xHO5)BOB8F*N|fL4RZ4Z#q4^MAe{Ol)993nu>+oFMdpfkDd? z3OCLo*Bi)r#Kp^1#qlXZlz0ZC?&8H@>&=ZH)Ok_MtxjLDN%i{1p1tzu6kG=R3W qF??s@VYT`y0}T4FW+0os%4}{B|HdTZ$jBJNctP0Y8v~dHjt~GGOoLMZ diff --git a/custom_components/powercalc/strategy/composite.py b/custom_components/powercalc/strategy/composite.py index 92ee7cf..6cb6b41 100644 --- a/custom_components/powercalc/strategy/composite.py +++ b/custom_components/powercalc/strategy/composite.py @@ -46,7 +46,7 @@ DEFAULT_MODE = CompositeMode.STOP_AT_FIRST def make_entity_id_optional(schema: vol.Schema) -> vol.Schema: """Make entity_id optional in schema.""" schema = schema.schema - schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore + schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore[index, attr-defined] return vol.Schema(schema) @@ -55,17 +55,17 @@ def get_numeric_state_schema() -> vol.Schema: return make_entity_id_optional(cv.NUMERIC_STATE_CONDITION_SCHEMA.validators[0]) -def get_state_condition_attribute_schema(value: Any) -> dict[str, Any]: # noqa: ANN401 +def get_state_condition_attribute_schema(value: object) -> dict[str, Any]: """Return the state attribute condition schema. We need to modify it to make entity_id optional.""" - return make_entity_id_optional(cv.STATE_CONDITION_ATTRIBUTE_SCHEMA)(value) # type: ignore + return make_entity_id_optional(cv.STATE_CONDITION_ATTRIBUTE_SCHEMA)(value) # type: ignore[no-any-return] -def get_state_condition_state_schema(value: Any) -> dict[str, Any]: # noqa: ANN401 +def get_state_condition_state_schema(value: object) -> dict[str, Any]: """Return the state condition schema. We need to modify it to make entity_id optional.""" - return make_entity_id_optional(cv.STATE_CONDITION_STATE_SCHEMA)(value) # type: ignore + return make_entity_id_optional(cv.STATE_CONDITION_STATE_SCHEMA)(value) # type: ignore[no-any-return] -def get_state_schema(value: Any) -> dict[str, Any]: # noqa: ANN401 +def get_state_schema(value: object) -> dict[str, Any]: """Validate a state condition.""" if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") # pragma: no cover @@ -158,7 +158,9 @@ class CompositeStrategy(PowerCalculationStrategyInterface): if isinstance(strategy, PlaybookStrategy): await self.activate_playbook(strategy) - if (entity_state.state == STATE_OFF and strategy.can_calculate_standby()) or entity_state.state != STATE_OFF: + if ( + entity_state.state == STATE_OFF and strategy.can_calculate_standby() + ) or entity_state.state != STATE_OFF: value = await strategy.calculate(entity_state) if value is not None: if self.mode == CompositeMode.STOP_AT_FIRST: @@ -186,8 +188,7 @@ class CompositeStrategy(PowerCalculationStrategyInterface): and manipulate the state """ for sub_strategy in self.strategies: - if hasattr(sub_strategy.strategy, "set_update_callback"): - sub_strategy.strategy.set_update_callback(update_callback) + sub_strategy.strategy.set_update_callback(update_callback) async def validate_config(self) -> None: """Validate correct setup of the strategy.""" @@ -204,7 +205,9 @@ class CompositeStrategy(PowerCalculationStrategyInterface): track_templates, ) - track_entities = [entity for sub_strategy in self.strategies for entity in sub_strategy.strategy.get_entities_to_track()] + track_entities = [ + entity for sub_strategy in self.strategies for entity in sub_strategy.strategy.get_entities_to_track() + ] return track_templates + track_entities def can_calculate_standby(self) -> bool: diff --git a/custom_components/powercalc/strategy/factory.py b/custom_components/powercalc/strategy/factory.py index 9176093..65916ba 100644 --- a/custom_components/powercalc/strategy/factory.py +++ b/custom_components/powercalc/strategy/factory.py @@ -171,11 +171,13 @@ class PowerCalculatorStrategyFactory: else: raise StrategyConfigurationError("No composite configuration supplied") - sub_strategies = composite_config + sub_strategies: list[ConfigType] mode = DEFAULT_MODE if isinstance(composite_config, dict): mode = composite_config.get(CONF_MODE, DEFAULT_MODE) - sub_strategies = composite_config.get(CONF_STRATEGIES) # type: ignore + sub_strategies = composite_config.get(CONF_STRATEGIES, []) + else: + sub_strategies = composite_config async def _create_sub_strategy(strategy_config: ConfigType) -> SubStrategy: condition_instance = None @@ -200,6 +202,8 @@ class PowerCalculatorStrategyFactory: ) return SubStrategy(condition_config, condition_instance, strategy_instance) # type: ignore + if not sub_strategies: + raise StrategyConfigurationError("No strategies configured for composite strategy") strategies = [await _create_sub_strategy(config) for config in sub_strategies] return CompositeStrategy(self._hass, strategies, mode) diff --git a/custom_components/powercalc/strategy/fixed.py b/custom_components/powercalc/strategy/fixed.py index 2707581..fc1b739 100644 --- a/custom_components/powercalc/strategy/fixed.py +++ b/custom_components/powercalc/strategy/fixed.py @@ -46,7 +46,7 @@ class FixedStrategy(PowerCalculationStrategyInterface): if self._per_state_power is not None: # Lookup by state if entity_state.state in self._per_state_power: - return await evaluate_power( + return evaluate_power( self._per_state_power.get(entity_state.state) or 0, ) @@ -55,12 +55,12 @@ class FixedStrategy(PowerCalculationStrategyInterface): if "|" in state_key: attribute, value = state_key.split("|", 2) if str(entity_state.attributes.get(attribute)) == value: - return await evaluate_power(power) + return evaluate_power(power) if self._power is None: return None - return await evaluate_power(self._power) + return evaluate_power(self._power) async def validate_config(self) -> None: """Validate correct setup of the strategy.""" @@ -84,8 +84,10 @@ class FixedStrategy(PowerCalculationStrategyInterface): track_templates.append(TrackTemplate(self._power, None, None)) if self._per_state_power: - for power in list(self._per_state_power.values()): - if isinstance(power, Template): - track_templates.append(TrackTemplate(power, None, None)) # noqa: PERF401 + track_templates.extend( + TrackTemplate(power, None, None) + for power in self._per_state_power.values() + if isinstance(power, Template) + ) return track_templates diff --git a/custom_components/powercalc/strategy/linear.py b/custom_components/powercalc/strategy/linear.py index 4359872..dccc25a 100644 --- a/custom_components/powercalc/strategy/linear.py +++ b/custom_components/powercalc/strategy/linear.py @@ -25,6 +25,8 @@ from custom_components.powercalc.const import ( CONF_GAMMA_CURVE, CONF_MAX_POWER, CONF_MIN_POWER, + CONF_POWER, + CONF_VALUE, ) from custom_components.powercalc.errors import StrategyConfigurationError from custom_components.powercalc.helpers import get_related_entity_by_device_class @@ -78,6 +80,8 @@ class LinearStrategy(PowerCalculationStrategyInterface): async def calculate(self, entity_state: State) -> Decimal | None: """Calculate the current power consumption.""" + value_entity = self.get_initialized_value_entity() + if not self._initialized: self._attribute = self.get_attribute(entity_state) self._initialized = True @@ -93,7 +97,7 @@ class LinearStrategy(PowerCalculationStrategyInterface): _LOGGER.debug( "%s: Linear mode state value: %d range(%d-%d)", - self._value_entity.entity_id, # type: ignore + value_entity.entity_id, value, min_value, max_value, @@ -115,9 +119,7 @@ class LinearStrategy(PowerCalculationStrategyInterface): def is_enabled(self, entity_state: State) -> bool: """Return if this strategy is enabled based on entity state.""" - if self._source_entity.domain == media_player.DOMAIN and entity_state.state is not STATE_PLAYING: # noqa: SIM103 - return False - return True + return not (self._source_entity.domain == media_player.DOMAIN and entity_state.state is not STATE_PLAYING) def get_min_calibrate(self, value: int) -> tuple[int, float]: """Get closest lower value from calibration table.""" @@ -134,16 +136,19 @@ class LinearStrategy(PowerCalculationStrategyInterface): calibrate = self._config.get(CONF_CALIBRATE) if isinstance(calibrate, dict): calibrate = [f"{key} -> {value}" for key, value in calibrate.items()] + elif isinstance(calibrate, list) and calibrate and isinstance(calibrate[0], dict): + calibrate = [f"{item[CONF_VALUE]} -> {item[CONF_POWER]}" for item in calibrate] if calibrate is None or len(calibrate) == 0: full_range = self.get_entity_value_range() min_value = full_range[0] max_value = full_range[1] min_power = self._config.get(CONF_MIN_POWER) or self._standby_power or 0 + max_power = self._config.get(CONF_MAX_POWER) + if max_power is None: # pragma: no cover + raise StrategyConfigurationError("Linear strategy must have max power defined") calibration_list.append((min_value, float(min_power))) - calibration_list.append( - (max_value, float(self._config.get(CONF_MAX_POWER))), # type: ignore - ) + calibration_list.append((max_value, float(max_power))) return calibration_list for line in calibrate: @@ -154,23 +159,30 @@ class LinearStrategy(PowerCalculationStrategyInterface): def get_entity_value_range(self) -> tuple: """Get the min/max range for a given entity domain.""" - if self._value_entity.domain == light.DOMAIN: # type: ignore + if self.get_initialized_value_entity().domain == light.DOMAIN: return 0, 255 return 0, 100 + def get_initialized_value_entity(self) -> SourceEntity: + """Return the initialized value entity.""" + if self._value_entity is None: # pragma: no cover + raise StrategyConfigurationError("Linear strategy has not been initialized") + return self._value_entity + def get_current_state_value(self, entity_state: State) -> int | None: """Get the current entity state, i.e. selected brightness.""" if self._attribute: return self.get_value_from_attribute(entity_state) - if self._value_entity.entity_id is not self._source_entity.entity_id: # type: ignore + value_entity = self.get_initialized_value_entity() + if value_entity.entity_id is not self._source_entity.entity_id: # If the value entity is different from the source entity, we need to fetch the state of the value entity - entity_state = self._hass.states.get(self._value_entity.entity_id) # type: ignore + entity_state = self._hass.states.get(value_entity.entity_id) if not entity_state: _LOGGER.error( "Value entity %s not found", - self._value_entity.entity_id, # type: ignore + value_entity.entity_id, ) return None @@ -184,7 +196,10 @@ class LinearStrategy(PowerCalculationStrategyInterface): return None def get_value_from_attribute(self, entity_state: State) -> int | None: - value: int | None = entity_state.attributes.get(self._attribute) # type: ignore[arg-type] + if self._attribute is None: # pragma: no cover + return None + + value = entity_state.attributes.get(self._attribute) if value is None: _LOGGER.warning( "No %s attribute for entity: %s", @@ -192,13 +207,15 @@ class LinearStrategy(PowerCalculationStrategyInterface): entity_state.entity_id, ) return None - if self._attribute == ATTR_BRIGHTNESS and value > 255: - value = 255 # Convert volume level to 0-100 range if self._attribute == ATTR_MEDIA_VOLUME_LEVEL: if entity_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True: - value = 0 - value *= 100 + return 0 + return int(float(value) * 100) + + value = int(value) + if self._attribute == ATTR_BRIGHTNESS and value > 255: + value = 255 return value def get_attribute(self, entity_state: State) -> str | None: @@ -214,9 +231,8 @@ class LinearStrategy(PowerCalculationStrategyInterface): if not self._config.get(CONF_CALIBRATE): if self._source_entity.domain not in ALLOWED_DOMAINS: raise StrategyConfigurationError( - "Entity domain not supported for linear mode. Must be one of: {}, or use the calibrate option".format( - ",".join(ALLOWED_DOMAINS), - ), + "Entity domain not supported for linear mode. " + f"Must be one of: {','.join(ALLOWED_DOMAINS)}, or use the calibrate option", "linear_unsupported_domain", ) if CONF_MAX_POWER not in self._config: @@ -235,7 +251,11 @@ class LinearStrategy(PowerCalculationStrategyInterface): async def get_value_entity(self) -> SourceEntity: """Set the value entity based on the current state.""" - if self._source_entity.domain in (vacuum.DOMAIN, lawn_mower.DOMAIN) and self._attribute is None and self._source_entity.entity_entry: + if ( + self._source_entity.domain in (vacuum.DOMAIN, lawn_mower.DOMAIN) + and self._attribute is None + and self._source_entity.entity_entry + ): # For vacuum cleaner and lawn mower, battery level is a separate entity related_entity = get_related_entity_by_device_class( self._hass, @@ -247,7 +267,7 @@ class LinearStrategy(PowerCalculationStrategyInterface): "No battery entity found for vacuum cleaner", "linear_no_battery_entity", ) - return await create_source_entity(related_entity, self._hass) + return create_source_entity(related_entity, self._hass) return self._value_entity or self._source_entity diff --git a/custom_components/powercalc/strategy/lut.py b/custom_components/powercalc/strategy/lut.py index 6395a8b..5ea2867 100644 --- a/custom_components/powercalc/strategy/lut.py +++ b/custom_components/powercalc/strategy/lut.py @@ -109,7 +109,11 @@ class LutRegistry: supported_modes = self._supported_modes.get(cache_key) if supported_modes is None: supported_modes = set() - for filename in await self._hass.async_add_executor_job(os.listdir, power_profile.get_model_directory()): + filenames = cast( + list[str], + await self._hass.async_add_executor_job(os.listdir, power_profile.get_model_directory()), + ) + for filename in filenames: if filename.endswith((".csv.gz", ".csv")): base_name = filename.split(".", 1)[0] supported_modes.add(LookupMode(base_name)) @@ -290,7 +294,8 @@ class LutStrategy(PowerCalculationStrategyInterface): color_temp = attrs.get(ATTR_COLOR_TEMP_KELVIN) if color_temp is None: _LOGGER.error( - "%s: Could not calculate power. no color temp set. Please check the attributes of your light in the developer tools.", + "%s: Could not calculate power. no color temp set. " + "Please check the attributes of your light in the developer tools.", entity_state.entity_id, ) return None @@ -300,12 +305,17 @@ class LutStrategy(PowerCalculationStrategyInterface): if color_mode == ColorMode.HS: try: original_color_mode = attrs.get(ATTR_COLOR_MODE) - hs = color_temperature_to_hs(attrs[ATTR_COLOR_TEMP_KELVIN]) if original_color_mode == ColorMode.COLOR_TEMP else attrs[ATTR_HS_COLOR] + hs = ( + color_temperature_to_hs(attrs[ATTR_COLOR_TEMP_KELVIN]) + if original_color_mode == ColorMode.COLOR_TEMP + else attrs[ATTR_HS_COLOR] + ) light_setting.hue = int(hs[0] / 360 * 65535) light_setting.saturation = int(hs[1] / 100 * 255) - except Exception: # noqa: BLE001 + except (KeyError, TypeError, ValueError): _LOGGER.error( - "%s: Could not calculate power. no hue/sat set. Please check the attributes of your light in the developer tools.", + "%s: Could not calculate power. no hue/sat set. " + "Please check the attributes of your light in the developer tools.", entity_state.entity_id, ) return None diff --git a/custom_components/powercalc/strategy/multi_switch.py b/custom_components/powercalc/strategy/multi_switch.py index c72fac5..54af0eb 100644 --- a/custom_components/powercalc/strategy/multi_switch.py +++ b/custom_components/powercalc/strategy/multi_switch.py @@ -44,7 +44,8 @@ class MultiSwitchStrategy(PowerCalculationStrategyInterface): async def calculate(self, entity_state: State) -> Decimal | None: if self.known_states is None: self.known_states = { - entity_id: (state.state if (state := self.hass.states.get(entity_id)) else STATE_UNAVAILABLE) for entity_id in self.switch_entities + entity_id: (state.state if (state := self.hass.states.get(entity_id)) else STATE_UNAVAILABLE) + for entity_id in self.switch_entities } if entity_state.entity_id != DUMMY_ENTITY_ID and entity_state.entity_id in self.switch_entities: @@ -60,7 +61,7 @@ class MultiSwitchStrategy(PowerCalculationStrategyInterface): return Decimal(sum(_get_power(state) for state in self.known_states.values())) def get_entities_to_track(self) -> list[str | TrackTemplate]: - return self.switch_entities # type: ignore + return [*self.switch_entities] def can_calculate_standby(self) -> bool: return self.off_power is not None diff --git a/custom_components/powercalc/strategy/playbook.py b/custom_components/powercalc/strategy/playbook.py index 8992333..5f64299 100644 --- a/custom_components/powercalc/strategy/playbook.py +++ b/custom_components/powercalc/strategy/playbook.py @@ -10,8 +10,8 @@ import gzip import logging import os -from homeassistant.const import STATE_OFF -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_OFF +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.typing import ConfigType @@ -64,6 +64,7 @@ class PlaybookStrategy(PowerCalculationStrategyInterface): self._update_callback: Callable[[Decimal], None] = lambda power: None self._start_time: datetime = dt.utcnow() self._cancel_timer: CALLBACK_TYPE | None = None + self._cancel_stop_listener: CALLBACK_TYPE | None = None self._config = config self._repeat: bool = bool(config.get(CONF_REPEAT)) self._autostart: str | None = config.get(CONF_AUTOSTART) @@ -116,6 +117,9 @@ class PlaybookStrategy(PowerCalculationStrategyInterface): if self._cancel_timer is not None: self._cancel_timer() self._cancel_timer = None + if self._cancel_stop_listener is not None: + self._cancel_stop_listener() + self._cancel_stop_listener = None def get_active_playbook(self) -> Playbook | None: """Get running playbook""" @@ -124,41 +128,79 @@ class PlaybookStrategy(PowerCalculationStrategyInterface): @callback def _execute_playbook_entry(self) -> None: """Execute one step of the playbook""" - if self._cancel_timer is not None: - self._cancel_timer() - self._cancel_timer = None + self._cancel_pending_timer() if not self._active_playbook: # pragma: no cover _LOGGER.error("Could not execute next playbook entry. No active playbook") return - queue = self._active_playbook.queue - if len(queue) == 0: - if self._repeat: - _LOGGER.debug("Playbook %s repeating", self._active_playbook.key) - self._start_time = dt.utcnow() - queue.reset() - self._execute_playbook_entry() - return - - _LOGGER.debug("Playbook %s completed", self._active_playbook.key) - self._active_playbook = None + playbook = self._active_playbook + if self._complete_or_repeat_playbook(playbook): return - entry = queue.dequeue() + entry = playbook.queue.dequeue() + self._schedule_playbook_entry_update(playbook, entry) + + @callback + def _cancel_pending_timer(self) -> None: + if self._cancel_timer is not None: + self._cancel_timer() + self._cancel_timer = None + + @callback + def _complete_or_repeat_playbook(self, playbook: Playbook) -> bool: + queue = playbook.queue + if len(queue) != 0: + return False + + if self._repeat: + _LOGGER.debug("Playbook %s repeating", playbook.key) + self._start_time = dt.utcnow() + queue.reset() + self._execute_playbook_entry() + return True + + _LOGGER.debug("Playbook %s completed", playbook.key) + self._active_playbook = None + return True + + @callback + def _schedule_playbook_entry_update(self, playbook: Playbook, entry: PlaybookEntry) -> None: + """Schedule the next playbook power update.""" @callback def _update_power(date_time: datetime) -> None: + active_playbook = self._active_playbook + if active_playbook is None: # pragma: no cover + return self._power = entry.power - _LOGGER.debug("playbook %s: Update power %.2f", self._active_playbook.key, self._power) # type: ignore + _LOGGER.debug("playbook %s: Update power %.2f", active_playbook.key, self._power) self._update_callback(self._power) - # Schedule next update self._execute_playbook_entry() + @callback + def _cancel_pending_updates_on_stop(_: datetime) -> None: + self._cancel_pending_timer() + if self._cancel_stop_listener is not None: + self._cancel_stop_listener() + self._cancel_stop_listener = None + self._active_playbook = None + + if self._cancel_stop_listener is not None: + self._cancel_stop_listener() + self._cancel_stop_listener = self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + _cancel_pending_updates_on_stop, + ) + # Schedule update in the future self._cancel_timer = async_track_point_in_time( self._hass, - _update_power, + HassJob( + _update_power, + name=f"powercalc playbook {playbook.key}", + cancel_on_shutdown=True, + ), self._start_time + timedelta(seconds=entry.time), ) @@ -167,20 +209,20 @@ class PlaybookStrategy(PowerCalculationStrategyInterface): if playbook_id in self._loaded_playbooks: return self._loaded_playbooks[playbook_id] - playbooks: dict[str, str] = self._config.get(CONF_PLAYBOOKS) # type: ignore + playbooks: dict[str, str] = dict(self._config.get(CONF_PLAYBOOKS) or {}) if playbook_id not in playbooks: raise StrategyConfigurationError( f"Playbook with id {playbook_id} not defined in playbooks config", ) file_path = os.path.join(self._playbook_directory, playbooks[playbook_id]) - if not (os.path.exists(file_path) or os.path.exists(f"{file_path}.gz")): - raise StrategyConfigurationError( - f"Playbook file '{file_path}' does not exist", - ) def _load_playbook_entries() -> list[PlaybookEntry]: """Load playbook entries from a CSV file, with support for gzipped files""" + if not (os.path.exists(file_path) or os.path.exists(f"{file_path}.gz")): + raise StrategyConfigurationError( + f"Playbook file '{file_path}' does not exist", + ) actual_path = file_path if os.path.exists(file_path) else f"{file_path}.gz" open_func = gzip.open if actual_path.endswith(".gz") else open with open_func(actual_path, mode="rt") as csv_file: diff --git a/custom_components/powercalc/strategy/strategy_interface.py b/custom_components/powercalc/strategy/strategy_interface.py index 1b66560..38c957a 100644 --- a/custom_components/powercalc/strategy/strategy_interface.py +++ b/custom_components/powercalc/strategy/strategy_interface.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable from decimal import Decimal from homeassistant.core import HomeAssistant, State @@ -28,5 +29,8 @@ class PowerCalculationStrategyInterface: """Return if this strategy is enabled based on entity state.""" return True + def set_update_callback(self, update_callback: Callable[[Decimal], None]) -> None: + """Register update callback to allow strategy to push power updates.""" + async def on_start(self, hass: HomeAssistant) -> None: """Called after HA has started""" diff --git a/custom_components/powercalc/strategy/wled.py b/custom_components/powercalc/strategy/wled.py index 7d46168..3d2e3cd 100644 --- a/custom_components/powercalc/strategy/wled.py +++ b/custom_components/powercalc/strategy/wled.py @@ -48,7 +48,11 @@ class WledStrategy(PowerCalculationStrategyInterface): self._estimated_current_entity: str | None = None async def calculate(self, entity_state: State) -> Decimal | None: - light_state = entity_state if entity_state.entity_id == self._light_entity.entity_id else self._hass.states.get(self._light_entity.entity_id) + light_state = ( + entity_state + if entity_state.entity_id == self._light_entity.entity_id + else self._hass.states.get(self._light_entity.entity_id) + ) if light_state.state in OFF_STATES and self._standby_power: return self._standby_power @@ -72,7 +76,7 @@ class WledStrategy(PowerCalculationStrategyInterface): self._power_factor, ) power = float(entity_state.state) / 1000 * self._voltage * self._power_factor - return await evaluate_power(power) + return evaluate_power(power) async def find_estimated_current_entity(self) -> str: entity_reg = entity_registry.async_get(self._hass) @@ -86,7 +90,9 @@ class WledStrategy(PowerCalculationStrategyInterface): if entity: return entity - raise StrategyConfigurationError("No estimated current entity found. Probably brightness limiter not enabled. See documentation") + raise StrategyConfigurationError( + "No estimated current entity found. Probably brightness limiter not enabled. See documentation", + ) def get_entities_to_track(self) -> list[str | TrackTemplate]: if self._estimated_current_entity: diff --git a/custom_components/powercalc/translations/cz.json b/custom_components/powercalc/translations/cz.json index d8f0e76..2209030 100644 --- a/custom_components/powercalc/translations/cz.json +++ b/custom_components/powercalc/translations/cz.json @@ -1,7 +1,7 @@ { "common": { - "source_entity": "Source entity", - "source_device": "Source device", + "source_entity": "Zdrojová entita", + "source_device": "Zdrojové zařízení", "remarks_smart_switch": "Tento profil poskytuje pouze hodnoty napájení v pohotovostním režimu. \n Pokud máte spotřebič, který zapnutí spotřebovává pevně dané množství energie, můžete zadat tuto hodnotu v dalším kroku v nastavení", "remarks_smart_dimmer": "Tento profil poskytuje pouze hodnoty spotřeby energie v pohotovostním režimu. \n V dalším kroku můžete volitelně zadat hodnoty připojeného světla" }, @@ -10,9 +10,9 @@ "already_configured": "Senzor je již nastaven, zadejte unikátní ID" }, "error": { - "daily_energy_mandatory": "You must supply at least one of Value or Value template", - "entity_mandatory": "Selecting an entity is required for any strategy other than playbook", - "fixed_mandatory": "You must supply at least one of Power, Power template or States power", + "daily_energy_mandatory": "Musíte zadat alespoň jednu z možností „Hodnota“ nebo „Šablona hodnoty“", + "entity_mandatory": "Pro všechny strategie kromě playbook je nutné vybrat entitu", + "fixed_mandatory": "Musíte zadat alespoň jednu z následujících možností: Power, šablonu Power nebo States power", "fixed_states_power_only": "This entity can only work with 'states_power' not 'power'", "group_mandatory": "You must define at least subgroups or power and energy-entities", "linear_mandatory": "You must supply at least one of max_power or calibrate", @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Create utility meters", + "daily_energy_value": "Daily energy value", "name": "Název", "on_time": "V čase", "start_time": "Start time", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Power", - "power_template": "Power template", - "states_power": "States power" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Pevná hodnota příkonu ve wattech, když je entita zapnutá", - "power_template": "Šablona bude vyhodnocena pro získání hodnoty příkonu ve wattech", - "states_power": "One 'state: power' pair on each row" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definujte fixní hodnotu příkonu pro vaši entitu. Případně můžete definovat hodnotu příkonu pro jednotlivé stavy. Například:\n\n`playing: 8.3`\n`paused: 2.25`", "title": "Fixed config" @@ -290,6 +287,7 @@ "attribute": "Specify the attribute. When left empty will be brightness for lights and percentage for fans", "calibrate": "Put a calibration value on each line. Example\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Lineární konfigurace" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Toggle when you want to keep repeating the playbook after it completes", "states_trigger": "Trigger a playbook based on a state change. Example\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Playbook config" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Name", "on_time": "On time", "start_time": "Start time", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "Power", - "power_template": "Power template", - "self_usage_included": "Self usage included", - "states_power": "States power" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Template will be evaluated to get the power value in W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "One 'state: power' pair on each row, see example above" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Start execution of a playbook.", @@ -860,6 +871,10 @@ }, "name": "Change GUI config" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/custom_components/powercalc/translations/de.json b/custom_components/powercalc/translations/de.json index 6e68fc9..7ed5828 100644 --- a/custom_components/powercalc/translations/de.json +++ b/custom_components/powercalc/translations/de.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Utilitymeter erstellen", + "daily_energy_value": "Daily energy value", "name": "Name", "on_time": "Dauer", "start_time": "Startzeit", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Leistung", - "power_template": "Leistung-Template", - "states_power": "Leistung je Zustand" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Ein fester Leistungswert in Watt, wenn die Einheit eingeschaltet ist", - "power_template": "Template wird ausgewertet, um den Leistungswert in W zu erhalten", - "states_power": "Ein 'state: power' Paar in jeder Zeile" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definieren Sie einen festen Leistungswert für Ihre Entität. Alternativ können Sie auch einen Leistungswert pro State definieren. Zum Beispiel:\n\n`playing: 8.3`\n`paused: 2.25`", "title": "Fixed Konfiguration" @@ -290,6 +287,7 @@ "attribute": "Geben Sie das Attribut an. Wenn es leer gelassen wird, wird Helligkeit für Lichter und Prozentsatz für Ventilatoren verwendet", "calibrate": "Geben Sie in jeder Zeile einen Kalibrierungswert an. Beispiel\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Linear Konfiguration" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Umschalten, wenn Sie das Playbook nach Abschluss wiederholen möchten", "states_trigger": "Auslösen eines Playbooks auf der Grundlage einer Zustandsänderung. Beispiel" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Playbook-Konfiguration" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Tägliche Energieeinstellungen", "data": { + "daily_energy_value": "Daily energy value", "name": "Name", "on_time": "Dauer", "start_time": "Startzeit", @@ -571,16 +572,11 @@ "fixed": { "title": "Feste Einstellungen", "data": { - "power": "Leistung", - "power_template": "Leistung-Template", - "self_usage_included": "Eigenverbrauch inklusive", - "states_power": "Leistung je Zustand" + "fixed_value": "Fixed value", + "self_usage_included": "Eigenverbrauch inklusive" }, "data_description": { - "power": "Ein fester Leistungswert in Watt, wenn die Entität eingeschaltet ist", - "power_template": "Template wird ausgewertet, um den Leistungswert in W zu erhalten", - "self_usage_included": "Ob der Leistungswert die vom intelligenten Schalter selbst verbrauchte Leistung enthält.", - "states_power": "Ein 'state: power' Paar in jeder Zeile, siehe Beispiel oben" + "self_usage_included": "Ob der Leistungswert die vom intelligenten Schalter selbst verbrauchte Leistung enthält." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Starten Sie die Ausführung eines Playbooks.", @@ -860,6 +871,10 @@ }, "name": "GUI-Konfiguration ändern" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Aktuelles laufendes Playbook abrufen", "name": "Aktuelles Playbook abrufen" diff --git a/custom_components/powercalc/translations/en.json b/custom_components/powercalc/translations/en.json index 5ca30c3..39572ae 100644 --- a/custom_components/powercalc/translations/en.json +++ b/custom_components/powercalc/translations/en.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Create utility meters", + "daily_energy_value": "Daily energy value", "name": "Name", "on_time": "On time", "start_time": "Start time", @@ -87,16 +88,12 @@ }, "fixed": { "data": { - "power": "Power", - "power_template": "Power template", - "states_power": "States power" + "fixed_value": "Power value" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Template will be evaluated to get the power value in W", - "states_power": "One 'state: power' pair on each row" + "fixed_value": "Power value in Watts when the entity is ON" }, - "description": "Define a fixed power value for your entity. Alternatively you can define a power value per state. For example:\n\n`playing: 8.3`\n`paused: 2.25`", + "description": "Define a fixed power value for your entity. See the [documentation]({docs_uri}) for more info", "title": "Fixed config" }, "global_configuration": { @@ -290,6 +287,7 @@ "attribute": "Specify the attribute. When left empty will be brightness for lights, volume for speakers, and percentage for fans", "calibrate": "Put a calibration value on each line. Example\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Linear config" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Toggle when you want to keep repeating the playbook after it completes", "states_trigger": "Trigger a playbook based on a state change. Example\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Playbook config" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Name", "on_time": "On time", "start_time": "Start time", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "Power", - "power_template": "Power template", - "self_usage_included": "Self usage included", - "states_power": "States power" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Template will be evaluated to get the power value in W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "One 'state: power' pair on each row" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Fixed value", + "value_template": "Template" + } + }, + "fixed_value": { + "choices": { + "power": "Fixed value", + "power_template": "Template", + "states_power": "State mapping" + } + } + }, "services": { "activate_playbook": { "description": "Start execution of a playbook.", @@ -860,6 +871,10 @@ }, "name": "Change GUI config" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/custom_components/powercalc/translations/es.json b/custom_components/powercalc/translations/es.json index 2ba61b5..aa17a5f 100644 --- a/custom_components/powercalc/translations/es.json +++ b/custom_components/powercalc/translations/es.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Crear contadores eléctricos", + "daily_energy_value": "Daily energy value", "name": "Nombre", "on_time": "Tiempo encendido", "start_time": "Hora de inicio", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Potencia", - "power_template": "Plantilla de potencia", - "states_power": "Estados de potencia" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Un valor de potencia fijo en Vatios (W) cuando la entidad está ENCENDIDA", - "power_template": "La plantilla será evaluada para obtener el valor de potencia en Vatios (W)", - "states_power": "Un par (\"estado: potencia\") en cada fila" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definir un valor de potencia fijo para tu entidad. Alternativamente, puedes definir un valor de potencia por estado. Por ejemplo: \n\n`playing: 8.3`\n`paused: 2.25`", "title": "Configuración fija" @@ -290,6 +287,7 @@ "attribute": "Especifique el atributo. Cuando se deja vacío será el brillo para las luces y porcentaje para los ventiladores", "calibrate": "Pon un valor de calibración en cada línea. Ejemplo\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configuración lineal" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Potencia para un solo interruptor cuando se enciende", "power_off": "Potencia para un solo interruptor cuando se apaga" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Configuración multiinterruptor" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Alternar cuando quieras seguir repitiendo la reproducción una vez completada", "states_trigger": "Dispara una reproducción basada en un cambio de estado. Ejemplo\n\nreproduciendo: programa1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configuración de Reproducciones" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Opciones de energía diarias", "data": { + "daily_energy_value": "Daily energy value", "name": "Nombre", "on_time": "Tiempo encendido", "start_time": "Hora de inicio", @@ -571,16 +572,11 @@ "fixed": { "title": "Opciones fijas", "data": { - "power": "Potencia", - "power_template": "Plantilla de potencia", - "self_usage_included": "Uso propio incluido", - "states_power": "Estados de potencia" + "fixed_value": "Fixed value", + "self_usage_included": "Uso propio incluido" }, "data_description": { - "power": "Un valor de potencia fijo en Vatios (W) cuando la entidad está ENCENDIDA", - "power_template": "La plantilla será evaluada para obtener el valor de potencia en Vatios (W)", - "self_usage_included": "Si el valor de potencia incluye la energía consumida por el interruptor inteligente mismo.", - "states_power": "Un par \"estado: potencia\" en cada fila" + "self_usage_included": "Si el valor de potencia incluye la energía consumida por el interruptor inteligente mismo." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Iniciar la ejecución de una reproducción.", @@ -860,6 +871,10 @@ }, "name": "Cambio configuración gráfica" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Obtener reproducción ejecutándose actualmente", "name": "Obtener reproducción activa" diff --git a/custom_components/powercalc/translations/fr.json b/custom_components/powercalc/translations/fr.json index 5ab0a0b..204ff6f 100644 --- a/custom_components/powercalc/translations/fr.json +++ b/custom_components/powercalc/translations/fr.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Créer des compteurs de services publics", + "daily_energy_value": "Daily energy value", "name": "Nom", "on_time": "Temps de fonctionnement", "start_time": "Heure de début", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Puissance", - "power_template": "Modèle de puissance", - "states_power": "Etat puissance" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Une valeur de puissance fixe en Watts lorsque l'entité est ON", - "power_template": "Le modèle sera évalué pour obtenir la valeur de puissance en W", - "states_power": "Une paire 'état : alimentation' sur chaque ligne" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Définissez une valeur de puissance fixe pour votre entité. Vous pouvez également définir une valeur de puissance par état. Par exemple :\n\n`lecture : 8.3`\n`pause : 2.25`", "title": "Configuration fixe" @@ -290,6 +287,7 @@ "attribute": "Spécifiez l'attribut. Lorsqu'il est laissé vide, la luminosité des lumières et le pourcentage des ventilateurs seront affichés.", "calibrate": "Mettez une valeur d'étalonnage sur chaque ligne. Exemple\n\n1 : 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configuration linéaire" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Basculer lorsque vous souhaitez continuer à répéter le playbook une fois qu'il est terminé", "states_trigger": "Trigger a playbook based on a state change. Example\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configuration Playbook" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Nom", "on_time": "Temps de fonctionnement", "start_time": "Start time", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "Puissance", - "power_template": "Modèle de puissance", - "self_usage_included": "Self usage included", - "states_power": "Etats de puissance" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Le modèle sera évalué pour obtenir la valeur de puissance en W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "Une paire 'état : alimentation' sur chaque ligne" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Démarrer l'exécution d'un playbook.", @@ -860,6 +871,10 @@ }, "name": "Calibrate utility meter" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/custom_components/powercalc/translations/it.json b/custom_components/powercalc/translations/it.json index 6d504a1..b0bc6e6 100644 --- a/custom_components/powercalc/translations/it.json +++ b/custom_components/powercalc/translations/it.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Crea contatori (utility meters)", + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "In tempo", "start_time": "Orario d'inizio", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Potenza", - "power_template": "Template di alimentazione", - "states_power": "Potenza degli Stati" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Un valore di potenza fissa in watt quando l'entità è attiva", - "power_template": "Il modello sarà valutato per ottenere il valore di potenza in W", - "states_power": "One 'state: power' pair on each row, see example above" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definisci un valore di potenza fisso per la tua entità. In alternativa puoi definire un valore di potenza per stato. Per esempio:\n\n`playing: 8.3`\n`pausa: 2.25`", "title": "Configurazione fissa" @@ -290,6 +287,7 @@ "attribute": "Specifica l'attributo. Quando lasciato vuoto sarà luminosità per le luci e percentuale per i ventilatori", "calibrate": "Inserire un valore di calibrazione su ogni riga. Esempio\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configurazione lineare" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Potenza di un singolo interruttore quando è acceso", "power_off": "Potenza di un singolo interruttore quando è acceso" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Configurazione multi-interruttore" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Attiva/disattiva quando si desidera continuare a ripetere il playbook dopo che è stato completato", "states_trigger": "Attiva un playbook in base a un cambio di stato. Esempio\n\nriproduzione: programma1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configurazione Playbook" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Opzioni energetiche giornaliere", "data": { + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "tempo stato ON", "start_time": "Tempo di inizio", @@ -571,16 +572,11 @@ "fixed": { "title": "Opzioni corrette", "data": { - "power": "Potenza", - "power_template": "Power template", - "self_usage_included": "Self usage included", - "states_power": "States power" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "Un valore di potenza fissa in watt quando l'entità è attiva", - "power_template": "Il modello sarà valutato per ottenere il valore di potenza in W", - "self_usage_included": "Il valore di potenza include l'energia consumata dallo smart switch stesso.", - "states_power": "One 'state: power' pair on each row, see example above" + "self_usage_included": "Il valore di potenza include l'energia consumata dallo smart switch stesso." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Avvia l'esecuzione di un playbook.", @@ -860,6 +871,10 @@ }, "name": "Cambia configurazione GUI" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Ottieni playbook attivo" diff --git a/custom_components/powercalc/translations/nl.json b/custom_components/powercalc/translations/nl.json index 5eb097e..d9736c0 100644 --- a/custom_components/powercalc/translations/nl.json +++ b/custom_components/powercalc/translations/nl.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Creëer utiliteit meters", + "daily_energy_value": "Daily energy value", "name": "Naam", "on_time": "Tijd aan per dag", "start_time": "Begintijd", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Vermogen", - "power_template": "Vermogen template", - "states_power": "Vermogen per status" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Een vastgestelde vermogen in Watt wanneer de gekoppelde entiteit AAN is", - "power_template": "Template die geëvalueerd wordt om het vermogen (W) te bepalen", - "states_power": "Eén 'status: vermogen' paar op elke rij" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definieer een vast vermogen (in W) voor je entiteit. Eventueel kan je ook een vermogen per status instellen. Bijvoorbeeld:\n\n`playing: 8.3`\n`paused: 2.25`", "title": "Gefixeerde configuratie" @@ -290,6 +287,7 @@ "attribute": "Specificeer een attribuut. Wanneer je dit leeg laat dan wordt helderheid gebruikt voor verlichting en percentage voor ventilatoren", "calibrate": "Kalibratiewaarde op iedere regel. Voorbeeld\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Lineaire configuratie" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Vermogen voor één schakelaar wanneer ingeschakeld", "power_off": "Vermogen voor één enkele schakelaar wanneer uitgeschakeld" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch configuratie" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Zet aan wanneer je wilt dat het playbook opnieuw wordt afgespeeld nadat het is beëindigd", "states_trigger": "Start een bepaald playbook gebaseerd op status. Voorbeeld\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Playbook configuratie" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Dagelijkse energie opties", "data": { + "daily_energy_value": "Daily energy value", "name": "Naam", "on_time": "Tijd aan per dag", "start_time": "Begintijd", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed opties", "data": { - "power": "Vermogen", - "power_template": "Vermogen template", - "self_usage_included": "Eigen verbruik inbegrepen", - "states_power": "Vermogen per status" + "fixed_value": "Fixed value", + "self_usage_included": "Eigen verbruik inbegrepen" }, "data_description": { - "power": "Een vastgestelde vermogen in Watt wanneer de gekoppelde entiteit AAN is", - "power_template": "Template die geëvalueerd wordt om het vermogen (W) te bepalen", - "self_usage_included": "Of het opgegeven vermogen ook het energieverbruik omvat dat door de slimme schakelaar zelf wordt verbruikt.", - "states_power": "Eén 'status: vermogen' paar op elke rij" + "self_usage_included": "Of het opgegeven vermogen ook het energieverbruik omvat dat door de slimme schakelaar zelf wordt verbruikt." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Start verwerking van een playbook.", @@ -860,6 +871,10 @@ }, "name": "GUI configuratie wijzigen" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Laad huidige actieve playbook", "name": "Laad actieve playbook" diff --git a/custom_components/powercalc/translations/pl.json b/custom_components/powercalc/translations/pl.json index 20c0e0d..2096323 100644 --- a/custom_components/powercalc/translations/pl.json +++ b/custom_components/powercalc/translations/pl.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Utwórz liczniki mediów (utility meter)", + "daily_energy_value": "Daily energy value", "name": "Nazwa", "on_time": "Sumaryczny czas włączenia", "start_time": "Moment uruchomienia", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Moc", - "power_template": "Szablon mocy", - "states_power": "Moce stanów" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Stała wartość mocy w watach, gdy encja jest WŁĄCZONA", - "power_template": "Szablon zostanie obliczony w celu uzyskania wartości mocy w watach", - "states_power": "Jedna para 'stan: moc' w każdym rzędzie" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Określ stałą wartość mocy dla swojej encji. Alternatywnie możesz zdefiniować wartość mocy dla każdego stanu. Na przykład:\n\n`odtwarzanie: 8.3`\n`pauza: 2.25`", "title": "Konfiguracja stała" @@ -290,6 +287,7 @@ "attribute": "Określ Atrybut. Gdy pole pozostanie puste, będzie to jasność dla świateł i procent dla wentylatorów", "calibrate": "Umieść wartość kalibracji w każdym wierszu. Przykład\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Konfiguracja liniowa" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Moc dla pojedynczego przełącznika, kiedy włączony", "power_off": "Moc dla pojedynczego przełącznika, kiedy wyłączony" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Konfiguracja wieloprzełącznika" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Przełącz, jeśli chcesz powtarzać playbook po jego zakończeniu", "states_trigger": "Wywołaj playbook na podstawie zmiany stanu. Przykład\n\nodtwarzanie: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Konfiguracja Playbook'ów" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Dzienne opcje energii", "data": { + "daily_energy_value": "Daily energy value", "name": "Nazwa", "on_time": "Sumaryczny czas włączenia", "start_time": "Czas rozpoczęcia", @@ -571,16 +572,11 @@ "fixed": { "title": "Opcje stałe", "data": { - "power": "Moc", - "power_template": "Szablon mocy", - "self_usage_included": "Zawarte własne użycie", - "states_power": "Moce stanów" + "fixed_value": "Fixed value", + "self_usage_included": "Zawarte własne użycie" }, "data_description": { - "power": "Stała wartość mocy w watach, gdy jednostka jest włączona", - "power_template": "Szablon zostanie obliczony w celu uzyskania wartości mocy w W", - "self_usage_included": "Czy wartość mocy obejmuje moc zużywaną przez sam inteligentny przełącznik.", - "states_power": "Jedna para 'stan: moc' w każdym rzędzie" + "self_usage_included": "Czy wartość mocy obejmuje moc zużywaną przez sam inteligentny przełącznik." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Rozpocznij wykonywanie playbooka.", @@ -860,6 +871,10 @@ }, "name": "Zmień konfigurację GUI" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Pobierz bieżący playbook", "name": "Pobierz aktywny playbook" diff --git a/custom_components/powercalc/translations/pt-BR.json b/custom_components/powercalc/translations/pt-BR.json index d9fdf54..b4cbbef 100644 --- a/custom_components/powercalc/translations/pt-BR.json +++ b/custom_components/powercalc/translations/pt-BR.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Criar medidores de utilidade", + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "Na hora", "start_time": "Hora de início", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Potência", - "power_template": "Modelo de potência", - "states_power": "Potência dos estados" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Um valor de potência fixo em Watts quando a entidade está LIGADA", - "power_template": "O modelo será avaliado para obter o valor da potência em W", - "states_power": "Um par 'estado: poder' em cada linha, veja o exemplo acima" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Defina um valor de potência fixo para sua entidade. Alternativamente, você pode definir um valor de potência por estado. Por exemplo:\n\n`playing: 8.3`\n`paused: 2.25`", "title": "Configuração fixa" @@ -290,6 +287,7 @@ "attribute": "Especifique o atributo. Quando deixado vazio, haverá brilho para luzes e porcentagem para ventiladores", "calibrate": "Coloque um valor de calibração em cada linha. Exemplo\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configuração linear" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Potência para um único interruptor quando ligado", "power_off": "Potência para um único interruptor quando desligado" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Opções de multi-interruptor" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Alterar quando você quiser continuar repetindo o playbook após ele terminar", "states_trigger": "Alterar o playbook de acordo com o estado. Exemplo\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configuração do Playbook" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Opções de consumo diário", "data": { + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "Na hora", "start_time": "Hora de início", @@ -571,16 +572,11 @@ "fixed": { "title": "Opções de Fixo", "data": { - "power": "Potência", - "power_template": "Modelo de potência", - "self_usage_included": "Auto uso incluído", - "states_power": "Potência dos estados" + "fixed_value": "Fixed value", + "self_usage_included": "Auto uso incluído" }, "data_description": { - "power": "Um valor de potência fixo em Watts quando a entidade está LIGADA", - "power_template": "O modelo será avaliado para obter o valor da potência em W", - "self_usage_included": "Se o valor de potência inclui a energia consumida pelo próprio interruptor inteligente.", - "states_power": "Um par 'estado: potência' em cada linha, veja o exemplo acima" + "self_usage_included": "Se o valor de potência inclui a energia consumida pelo próprio interruptor inteligente." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Iniciar a execução de um playbook.", @@ -860,6 +871,10 @@ }, "name": "Calibrate utility meter" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Obter o atual playbook em execução", "name": "Obter playbook ativo" diff --git a/custom_components/powercalc/translations/pt.json b/custom_components/powercalc/translations/pt.json index 8a5ae95..58a10af 100644 --- a/custom_components/powercalc/translations/pt.json +++ b/custom_components/powercalc/translations/pt.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Create utility meters", + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "Na hora", "start_time": "Hora de início", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Potência", - "power_template": "Modelo de potência", - "states_power": "Potência dos Estados" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Um valor de potência fixo em Watts quando a entidade está LIGADA", - "power_template": "O modelo será avaliado para obter o valor da potência em W", - "states_power": "Um par 'estado: potência' em cada linha, veja o exemplo acima" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Defina um valor de potência fixo para sua entidade. Alternativamente, você pode definir um valor de energia por estado. Por exemplo:\n\n`reproduzindo: 8.3`\n`pausado: 2.25`", "title": "Configuração fixa" @@ -290,6 +287,7 @@ "attribute": "Especifique o atributo. Quando deixado vazio, haverá brilho para luzes e percentagem para ventoinhas", "calibrate": "Coloque um valor de calibração em cada linha. Exemplo\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configuração linear" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Alternar se pretende que a sequência reinicie e repita após terminar", "states_trigger": "Trigger a playbook based on a state change. Example\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configuração de Sequência" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Nome", "on_time": "Na hora", "start_time": "Start time", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "Potência", - "power_template": "Modelo de energia", - "self_usage_included": "Self usage included", - "states_power": "Potência dos Estados" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "O modelo será avaliado para obter o valor da potência em W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "Um par 'estado: ptência' em cada linha, veja o exemplo acima" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Iniciar execução de uma Sequência.", @@ -860,6 +871,10 @@ }, "name": "Calibrate utility meter" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/custom_components/powercalc/translations/ro.json b/custom_components/powercalc/translations/ro.json index 15add71..28ef62f 100644 --- a/custom_components/powercalc/translations/ro.json +++ b/custom_components/powercalc/translations/ro.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Creați contoare", + "daily_energy_value": "Daily energy value", "name": "Nume", "on_time": "Timp pornit", "start_time": "Timpul de pornire", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Putere", - "power_template": "Sablon de putere", - "states_power": "Puterea starilor" + "fixed_value": "Fixed value" }, "data_description": { - "power": "O valoare fixă a puterii în wați când entitatea este PORNITĂ", - "power_template": "Șablonul va fi evaluat pentru a obține valoarea puterii în W", - "states_power": "O pereche 'stare: putere' pe fiecare rând, vezi exemplul de mai sus" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definiți o valoare fixă a puterii pentru entitatea dvs. Alternativ, puteți defini o valoare de putere per stare. De exemplu:\n\n`redare: 8,3`\n`în pauză: 2,25`", "title": "Configurație fixă" @@ -290,6 +287,7 @@ "attribute": "Specificați atributul. Când este lăsat gol, va fi luminozitate pentru lumini și procent pentru ventilatoare", "calibrate": "Pune o valoare de calibrare pe fiecare linie. Exemplu\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Configurație liniară" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Power for a single switch when turned on", "power_off": "Power for a single switch when turned off" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi switch config" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Comutați când doriți să continuați să repetați manualul după finalizarea acestuia", "states_trigger": "Trigger a playbook based on a state change. Example\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Configurare Playbook" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Nume", "on_time": "Timp pornit", "start_time": "Start time", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "Putere", - "power_template": "Șablon de putere", - "self_usage_included": "Self usage included", - "states_power": "Putere de stare" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Șablonul va fi evaluat pentru a obține valoarea puterii în W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "O pereche 'stare: putere' pe fiecare rând, vezi exemplul de mai sus" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Începeți execuția unui playbook.", @@ -860,6 +871,10 @@ }, "name": "Schimbați configurația GUI" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/custom_components/powercalc/translations/ru.json b/custom_components/powercalc/translations/ru.json index 986b8aa..1a81625 100644 --- a/custom_components/powercalc/translations/ru.json +++ b/custom_components/powercalc/translations/ru.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Создать счётчики", + "daily_energy_value": "Daily energy value", "name": "Название", "on_time": "Время работы", "start_time": "Время начала", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Мощность", - "power_template": "Шаблон мощности", - "states_power": "Мощность по состояниям" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Фиксированное значение мощности в ваттах, когда объект включён", - "power_template": "Шаблон для вычисления значения мощности в ваттах", - "states_power": "Пара 'состояние: мощность' на каждой строке, см. [документацию](https://docs.powercalc.nl/strategies/fixed/#power-per-state)" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Определите фиксированное значение мощности для вашего объекта. Альтернативно можно указать мощность для каждого состояния. Например:\n\n`playing: 8.3`\n`paused: 2.25`", "title": "Фиксированная конфигурация" @@ -290,6 +287,7 @@ "attribute": "Укажите атрибут. Если оставить пустым, будет использоваться яркость для светильников, громкость для колонок и процент для вентиляторов", "calibrate": "Укажите калибровочное значение на каждой строке. Пример\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Линейная конфигурация" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Мощность одного выключателя во включённом состоянии", "power_off": "Мощность одного выключателя в выключенном состоянии" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Конфигурация мультивыключателя" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Включите, если хотите повторять сценарий после его завершения", "states_trigger": "Запуск сценария по изменению состояния. Пример\n\nplaying: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Конфигурация сценариев" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Параметры суточной энергии", "data": { + "daily_energy_value": "Daily energy value", "name": "Название", "on_time": "Время работы", "start_time": "Время начала", @@ -571,16 +572,11 @@ "fixed": { "title": "Фиксированные параметры", "data": { - "power": "Мощность", - "power_template": "Шаблон мощности", - "self_usage_included": "Включено собственное потребление", - "states_power": "Мощность по состояниям" + "fixed_value": "Fixed value", + "self_usage_included": "Включено собственное потребление" }, "data_description": { - "power": "Фиксированное значение мощности в ваттах, когда объект включён", - "power_template": "Шаблон для вычисления значения мощности в ваттах", - "self_usage_included": "Включено ли в значение мощности потребление самого умного выключателя", - "states_power": "Пара 'состояние: мощность' на каждой строке, см. [документацию](https://docs.powercalc.nl/strategies/fixed/#power-per-state)" + "self_usage_included": "Включено ли в значение мощности потребление самого умного выключателя" } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Запустить выполнение сценария.", @@ -860,6 +871,10 @@ }, "name": "Изменить конфигурацию GUI" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Получить текущий запущенный сценарий", "name": "Получить активный сценарий" diff --git a/custom_components/powercalc/translations/sk.json b/custom_components/powercalc/translations/sk.json index 94bb5d9..1c5db6a 100644 --- a/custom_components/powercalc/translations/sk.json +++ b/custom_components/powercalc/translations/sk.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Vytvorenie meracích prístrojov", + "daily_energy_value": "Daily energy value", "name": "Názov", "on_time": "V čase", "start_time": "Štart čas", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Výkon", - "power_template": "Šablóna výkonu", - "states_power": "Stavy výkonu" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Pevná hodnota výkonu vo wattoch, keď je entita ZAPNUTÁ", - "power_template": "Šablóna sa vyhodnotí, aby sa získala hodnota výkonu vo W", - "states_power": "Jeden pár „stav: výkon“ v každom riadku, pozri príklad vyššie" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definujte pevnú hodnotu výkonu pre vašu entitu. Prípadne môžete definovať hodnotu výkonu pre každý stav. Napríklad:\n\n`prehrávanie: 8.3`\n`pozastavené: 2.25`", "title": "Opravená konfigurácia" @@ -290,6 +287,7 @@ "attribute": "Zadajte atribút. Keď zostane prázdne, bude jas pre svetlá a percento pre ventilátory", "calibrate": "Na každý riadok uveďte kalibračnú hodnotu. Príklad\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Lineárna konfigurácia" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Napájanie jedným vypínačom pri zapnutí", "power_off": "Napájanie na jeden vypínač pri vypnutí" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Konfigurácia viacerých prepínačov" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Prepínač, keď chcete pokračovať v opakovaní príručky po jej dokončení", "states_trigger": "Spustite playbook na základe zmeny stavu. Príklad\n\nprehrávanie: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Playbook konfigurácia" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Denné energetické možnosti", "data": { + "daily_energy_value": "Daily energy value", "name": "Názov", "on_time": "V čase", "start_time": "Čas začiatku", @@ -571,16 +572,11 @@ "fixed": { "title": "Pevné možnosti", "data": { - "power": "Výkon", - "power_template": "Výkon šablóna", - "self_usage_included": "Samostatné použitie v cene", - "states_power": "Výkonové stavy" + "fixed_value": "Fixed value", + "self_usage_included": "Samostatné použitie v cene" }, "data_description": { - "power": "Pevná hodnota výkonu vo wattoch, keď je entita ZAPNUTÁ", - "power_template": "Šablóna sa vyhodnotí, aby sa získala hodnota výkonu vo W", - "self_usage_included": "Či hodnota výkonu zahŕňa výkon spotrebovaný samotným inteligentným prepínačom.", - "states_power": "Jeden pár „stav: výkon“ v každom riadku, pozri príklad vyššie" + "self_usage_included": "Či hodnota výkonu zahŕňa výkon spotrebovaný samotným inteligentným prepínačom." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Spustenie vykonávania playbooku.", @@ -860,6 +871,10 @@ }, "name": "Zmeniť GUI nastavenie" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Získajte aktuálny playbook", "name": "Získať aktuálny playbook" diff --git a/custom_components/powercalc/translations/sv.json b/custom_components/powercalc/translations/sv.json index 8339611..6807d7a 100644 --- a/custom_components/powercalc/translations/sv.json +++ b/custom_components/powercalc/translations/sv.json @@ -48,6 +48,7 @@ "daily_energy": { "data": { "create_utility_meters": "Skapa bruksmätare", + "daily_energy_value": "Daily energy value", "name": "Namn", "on_time": "Igång tid", "start_time": "Starttid", @@ -87,14 +88,10 @@ }, "fixed": { "data": { - "power": "Effekt", - "power_template": "Effektmall", - "states_power": "Effekttillstånd" + "fixed_value": "Fixed value" }, "data_description": { - "power": "Ett fast värde i Watt när entiteten är igång", - "power_template": "Mall för att beräkna effekten i W", - "states_power": "En 'tillstånd:effekt' par per rad, se exempel ovan" + "fixed_value": "Fixed value in Watts when the entity is ON" }, "description": "Definiera ett fast värde för din entitet. Alternativt kan du definiera ett värde per tillstånd. Till exempel: \n\n`spelar: 8.3`\n`pausad: 2.25`", "title": "Fast konfig" @@ -290,6 +287,7 @@ "attribute": "Ange attributet. Om tomt används ljusstyrka för lampor och procent för fläktar", "calibrate": "Skriv ett kalibreringsvärde per rad. Exempel\n\n1: 20" }, + "description": "Define the linear power calculation options. See the [documentation]({docs_uri}) for more information. Use either min/max power or calibration values which allows for more points of control.", "title": "Linjär konfiguration" }, "manufacturer": { @@ -327,6 +325,7 @@ "power": "Effekt för enstaka enhet när påslagen", "power_off": "Effekt för enstaka enhet när avslagen" }, + "description": "Define the Multi switch power calculation options. See the [documentation]({docs_uri}) for more information.", "title": "Multi-enhetskonfiguration" }, "playbook": { @@ -341,6 +340,7 @@ "repeat": "Växla om du vill att playbooken ska repeteras när den är färdig", "states_trigger": "Utlösa en spelbok baserad på en tillståndsändring. Exempel\n\nspelar: program1" }, + "description": "Define the playbook options. See the [documentation]({docs_uri}) for more information.", "title": "Spelbok konfiguration" }, "power_advanced": { @@ -542,6 +542,7 @@ "daily_energy": { "title": "Daily energy options", "data": { + "daily_energy_value": "Daily energy value", "name": "Namn", "on_time": "Igång tid", "start_time": "Starttid", @@ -571,16 +572,11 @@ "fixed": { "title": "Fixed options", "data": { - "power": "effekt", - "power_template": "Effektmall", - "self_usage_included": "Self usage included", - "states_power": "Effekttillstånd" + "fixed_value": "Fixed value", + "self_usage_included": "Self usage included" }, "data_description": { - "power": "A fixed power value in Watts when the entity is ON", - "power_template": "Mall för att beräkna effekten i W", - "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself.", - "states_power": "En 'tillstånd:effekt' par per rad, se exempel ovan" + "self_usage_included": "Whether the power value includes the power consumed by the smart switch itself." } }, "global_configuration": { @@ -815,6 +811,21 @@ } } }, + "selector": { + "daily_energy_value": { + "choices": { + "value": "Value", + "value_template": "Value template" + } + }, + "fixed_value": { + "choices": { + "power": "Power", + "power_template": "Power template", + "states_power": "States power" + } + } + }, "services": { "activate_playbook": { "description": "Starts körningen sv en playbook.", @@ -860,6 +871,10 @@ }, "name": "Ändra GUI-konfiguration" }, + "debug_group": { + "description": "Get a debug overview of a group power or energy sensor including current member values.", + "name": "Debug group" + }, "get_active_playbook": { "description": "Get current running playbook", "name": "Get active playbook" diff --git a/zigbee2mqtt/coordinator_backup.json b/zigbee2mqtt/coordinator_backup.json index 6006cda..35a49a8 100644 --- a/zigbee2mqtt/coordinator_backup.json +++ b/zigbee2mqtt/coordinator_backup.json @@ -4,7 +4,7 @@ "version": 1, "source": "zigbee-herdsman@10.4.0", "internal": { - "date": "2026-06-14T02:45:15.859Z", + "date": "2026-06-15T13:46:01.495Z", "znpVersion": 1 } }, @@ -25,7 +25,7 @@ "network_key": { "key": "0a7ed3caf6adb653119697df9d04ec25", "sequence_number": 0, - "frame_counter": 408907 + "frame_counter": 480476 }, "devices": [ { @@ -35,7 +35,7 @@ "link_key": { "key": "a59c7f704c547dca7033b287b04ffc4b", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -45,7 +45,7 @@ "link_key": { "key": "91fa9ab38f0a04fc102f357e78f61f7d", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -55,7 +55,7 @@ "link_key": { "key": "a59a9f9bbb354104241b4a3476c2bd1f", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -65,7 +65,7 @@ "link_key": { "key": "d6d47144ee36ca67791986b8f5b74bb2", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -75,7 +75,7 @@ "link_key": { "key": "9b47a21f8d33962956b05e040cb24386", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -85,7 +85,7 @@ "link_key": { "key": "3537b9fd0d00b2cdf8c045e68c816762", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 55 } }, { @@ -95,7 +95,7 @@ "link_key": { "key": "85676341f405baf92aaa94bdef843b2c", "rx_counter": 0, - "tx_counter": 0 + "tx_counter": 22 } }, { @@ -105,7 +105,7 @@ "link_key": { "key": "fea770704c547dca2b08bd87b04ffc4b", "rx_counter": 0, - "tx_counter": 22 + "tx_counter": 44 } }, { @@ -115,7 +115,7 @@ "link_key": { "key": "facb3a7e78f61f7d7b1e95b38f0a04fc", "rx_counter": 0, - "tx_counter": 11 + "tx_counter": 33 } }, { @@ -125,7 +125,7 @@ "link_key": { "key": "452689b8f5b74bb2eaeb7e44ee36ca67", "rx_counter": 0, - "tx_counter": 0 + "tx_counter": 22 } } ]

^&gIXdUM(B{||TLlm55Y>JES9I`YqhkNAIz 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 Rd&suWa9$2=H0I1k{~vX8n=AkT delta 3145 zcmaJ@S!^5U5&oCUUGXNxQzuE?rtZXwEIT?Z%f~XZY=(+t*p*Dv;_7fHQuz7HiQGeK z3)FFo#Bu%t1yZ0@gQ8U)S`^4jANtTdv?%%{N2)Dst=8ye4!ecGmHtW74eBD~uedn4Jb6aga~TO)Ay-LUh!RC$LI`<7 z)S!q=RE*51uf?dO$V^sBSczg{CdJImiiKGeE3@i)W7MYDnO*0ls6%lwXQ&xpBu8CJ zDJ#_#B~dr?fXo#2Dn91ZW#*_~39x%VE060-OEjp2SV#%8uu{WnbeT0;tJJYNowr5n zl?K)jB9mfM;UpbSd(&u9Ra;37U%B^KG73<1E2iM1`F`x$lF`HguF9Y z=~{bLQR0kU0UIx07fr?kwPcz!$0qoXNiFw^Yuny^fV?Bp*NimdNKG4BC83u)HS;e~8P5vjbI zGyJzDv#D0tY9HLWS|BTApV$QVE|RtO9en({S%0j*p3tRxc(SR&9+4+9+>kkG@4vvT zkf&JaARWSjVVuN7JU9Q9)nZK$@)r2gu1hcR!E$f&ad4Rd=PEKBAY|M)o#(587boz= z`ZZx9gnzxfV!6|Z4k_s4Dst)6gtMRrZ@`XN0Ld+yc#to2P z;a23-;#@ivX0vK|Dw~;}n+Y#wAcsG&`Nm8*Mu;ZOF?Bviu>&dYgO(z}Ez=SpnwVoW zBGCpEppho$U!;j|s!N)9GnvjwAZ~07hr_gm$LyiLE?Dc8$O5xM{wWi4c-6fl8iMl5 z*&UhqLJz(5_r3KGyp4BE?}%Gg$M@RqwLP$gR)>Ckk^3A25^V!dt~wfo^ZYxGuNA)8TY1lWi`o8gf|MAp(4ptO~ra5f1e{JOJVXydn?uT=Da_+x`+ z%&F;Vii;c;zp)WE{lMAg9zhA7c!g|5+TJtMHojEWZ~j=K7@Rin30LJP#-%nSUlbN~ zCxFD@&IN%W61|dT7s+F9WJiK0$gbKBr zSxU?&GpQuY(j~x{7d)sBi3173Nb`&iDQ89o6Q>8G=PxJDTsRXycNxl!aMa5UmUjSooGNA!FOnUf$<_ zRX&OADF2$jvKPUoS1ph2Zd@Wc4|fjx^;_g$20?1ymjP0Hh_t*l+UQ+0!~f&|YIi*> zb>vf+O9hbO9C{(r>UgX&U{4%^*)0DofK9a@=&b2QcYV|Sz!f_Rr6+-CA@t^nGLw52 z^@aXwZZGuL!1559zNne5g29{U8wP^SBum!-*htrVMt1Vd<*B?R5A7mI!2@U0Hwm%t ztpihlb`km$27atOYA)C>82CVWK(O+f3Xk1Afo-l|F$awOglMAWe7&N^hoeKY=hRFt zOB4B7NL_rtA{5c#a1qqc)E|fDGxdj}4p$!S&<};+K&1YywBeDhY0Kui8@&^KXsf$# ztJ|!9^$)h*JxO$1-jR1q{7GeVa##*W+WCQ zRv=pSvo6rmN$7A!ml8O%xmbO@1ils7v{uRAtFGj?tA}|w*zBIl&d+Bv^l3Ad^Xg!i zjNdt}6D+HEF6a{~`JG@}ZxseZ;A-;d)T}z6)Jzu_*jzS~Ol$H`R;5#_CPB0$(_C+; z^9yO1iKA*-or3AY?V)#tDgOOX)iRbtlhwDGnn~%stIrv|o%BroUkWu-FZ^{ZrqzCW z8$1y@xsQPEh=L$&liDYw;R(6*klflPUE8F2o4oc%QucRpYTsZI4EqGgE|PsW5$1&Z zWcJK98Q&&fdLp|1Y)n25*8K3shc_OZtaokSw!MF4!xUbv`)Tt>&1+*DwMVz|;&tDV z4O3*T=V8m~&st7zw44Pw-wyxW5%Np&o>&$#tdq)}m-&Y^u^{~B><7yP_}T#3@3sh7 JW2AbG{|9?Ieft0a diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/global_configuration.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/global_configuration.cpython-314.pyc index ccf0a9986cb9649c75420d205adc5fadb8736d7b..4ed1e726d3b3940ac11b5b25c6dbf334c5c85394 100644 GIT binary patch delta 936 zcmZvaT}YE*6vv67cx+uEnBJ_nWE21EfP=iP!Ke~|WCMFt-BNj5c_<%zCb5lr|(s8`;KaR3R!`bV0^@Q5Ko ze?x}9`zGav@d1|wA2Sa2bf1r1Xo&apH%Ai}<1%_Bzj9WZ7a)QW>ub1#LMFnXGM+iY zff0S#Q5eLV*$XhHblHsnx3S;xkksY83VO5#++}^ORO21`68R+8Sc=(2(~2rfw$h|X zF{xvw+!ehYmx4Csr&AOxn*Al&zfnR`!ck9-;&pkzJGqtPP&p(ThvEjup>Zzfi@Zg% z-`nIUqaHhWr?xVjDuYC0P~2cRiQ!Vd6yD`6ntghcLABK4n{pqn7yB``(`OKw?HH^G zCOlno%e%alou{%%G&aQzcAmzr^c~_Ynq8I6K2>(F%H@~|J1QQRjH$!|k^%OE12HG1 zqlu+8V@y7mk z_qL9nu4r2#5lhHz-EyE^dwXZ1-EvahR554UvnKLJ^#&x{2Xo;&b0G%m zianBgVw6UtA!$zX&)KT~=dzN^lO^g_`Rp=oh7~6R_`Pm9trLK*SJkysJTBGeW*pbC QoNz)%wN8a>E!*sW0OQdFK>z>% delta 955 zcmb7@T}TvB6vxk*kGs3muC5=eX|C07sg=5Ay5Lrpp=)JG4P*q7ZMtbWCb?4)q=plrlj|U$r<|PU$qM(Rk3d0inc*vrN*u`QYdg#s+;cNHdckUVf|9j54%soGZ zgF~3HD8r)wU)q(@-L*^3WNgS>Q_ij4vfwNZ;jO~QAFY|GI&W&~RaE$wDV@VDz4k`2@uZc1%TZNB6os|<-Xbc?{z5qQ6 z`K4|?lzp9S@mqm^G(J{sL17Z8zWN#>rK&HYKvZ*wQw^U(W_%966p+!~+Mk$1w<5*Z zZM=>=N1lupGG zlmDznsBVTh>#+oo6cO5ss4eGZTf1= zl{-eA?Pk1ZEPm)k{16Q^loanoCCR|$KII;Co4&BQs$XBItN~K4p@@xE-vwT uWHu#cE)+~CNe1Xw!=$H`zp)fp{N5+fx5h$mOooC;g7G1qRON0i%KHs%coo$E diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/group.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/group.cpython-314.pyc index f2334309dc09194e90a249a2f157c41180581928..675f1bb5a14b67580c7e66bc20a724d2dbd8d5f8 100644 GIT binary patch delta 7493 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 VcmB3DQ7C93{7vUQleN3t`u{Aeq*4F? diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/library.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/library.cpython-314.pyc index 7aace5e0cd57855c7848933c313f87247e6deda1..b6fc81e5f4417848acdcfacbb2c2bfc79dd5c7d8 100644 GIT binary patch delta 8530 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 riT>uv7ZW2vW^BT^ delta 8760 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 delta 188 zcmZos`me~V&Bx2d00cS9)U&ou z#?;C6j69n^GDG}E0DU!ZO!B) zoROcKnpj+%SzMBsSE84kpI2N`#0Ipoh#f?50Et_&sA^IYOA_N#^Gb>;wjF U#>jb}!R;c0+g%2e$&v!E0a)fa2mk;8 diff --git a/custom_components/powercalc/flow_helper/flows/__pycache__/virtual_power.cpython-314.pyc b/custom_components/powercalc/flow_helper/flows/__pycache__/virtual_power.cpython-314.pyc index 42a5bba571a6e0faba732eeb75e6ce5e6e3d99b1..ef45455ccd37e257491cc8afae79cda7010368cf 100644 GIT binary patch literal 36776 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 amDJy(FmkV7+eg!8uGIaH6h;V7{r>>zbBK!o delta 10072 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 str: + """Infer the daily energy config key from a validated ChooseSelector value.""" + return CONF_VALUE_TEMPLATE if isinstance(value, str) else CONF_VALUE + + def build_daily_energy_config(user_input: dict[str, Any], schema: vol.Schema) -> dict[str, Any]: """Build the config under daily_energy: key.""" + user_input = unwrap_choose_selector( + dict(user_input), + CONF_DAILY_ENERGY_VALUE, + daily_energy_choice_key_from_validated_value, + ) config: dict[str, Any] = { CONF_DAILY_FIXED_ENERGY: {}, } + schema_keys = {key.schema if isinstance(key, vol.Marker) else key for key in schema.schema} + schema_keys.discard(CONF_DAILY_ENERGY_VALUE) + schema_keys |= {CONF_VALUE, CONF_VALUE_TEMPLATE} for key, val in user_input.items(): - if key in schema.schema and val is not None: + if key in schema_keys and val is not None: if key in {CONF_CREATE_UTILITY_METERS, CONF_GROUP, CONF_NAME, CONF_UNIQUE_ID}: config[str(key)] = val continue @@ -80,14 +119,19 @@ class DailyEnergyConfigFlow: async def async_step_daily_energy( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the flow for daily energy sensor.""" self.flow.selected_sensor_type = SensorType.DAILY_ENERGY - async def _validate(user_input: dict[str, Any]) -> dict[str, Any]: - if CONF_VALUE not in user_input and CONF_VALUE_TEMPLATE not in user_input: + def _validate(user_input: dict[str, Any]) -> dict[str, Any]: + unwrapped = unwrap_choose_selector( + dict(user_input), + CONF_DAILY_ENERGY_VALUE, + daily_energy_choice_key_from_validated_value, + ) + if CONF_VALUE not in unwrapped and CONF_VALUE_TEMPLATE not in unwrapped: raise SchemaFlowError("daily_energy_mandatory") - return build_daily_energy_config(user_input, SCHEMA_DAILY_ENERGY) + return build_daily_energy_config(unwrapped, SCHEMA_DAILY_ENERGY) return await self.flow.handle_form_step( PowercalcFormStep( @@ -104,10 +148,21 @@ class DailyEnergyOptionsFlow: def __init__(self, flow: PowercalcOptionsFlow) -> None: self.flow: PowercalcOptionsFlow = flow - async def async_step_daily_energy(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_daily_energy(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the daily energy options flow.""" + form_data = wrap_choose_selector( + dict(self.flow.sensor_config[CONF_DAILY_FIXED_ENERGY]), + CONF_DAILY_ENERGY_VALUE, + DAILY_ENERGY_VALUE_CHOICES, + ) schema = fill_schema_defaults( SCHEMA_DAILY_ENERGY_OPTIONS, - self.flow.sensor_config[CONF_DAILY_FIXED_ENERGY], + form_data, ) + if user_input is not None: + user_input = unwrap_choose_selector( + dict(user_input), + CONF_DAILY_ENERGY_VALUE, + daily_energy_choice_key_from_validated_value, + ) return await self.flow.async_handle_options_step(user_input, schema, Step.DAILY_ENERGY) diff --git a/custom_components/powercalc/flow_helper/flows/global_configuration.py b/custom_components/powercalc/flow_helper/flows/global_configuration.py index 770ee06..d1ca650 100644 --- a/custom_components/powercalc/flow_helper/flows/global_configuration.py +++ b/custom_components/powercalc/flow_helper/flows/global_configuration.py @@ -3,8 +3,8 @@ from __future__ import annotations from datetime import timedelta from typing import TYPE_CHECKING, Any +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ENABLED, CONF_SENSORS, UnitOfTime -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.typing import ConfigType import voluptuous as vol @@ -44,7 +44,11 @@ from custom_components.powercalc.const import ( ENTRY_GLOBAL_CONFIG_UNIQUE_ID, ) from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step -from custom_components.powercalc.flow_helper.schema import SCHEMA_ENERGY_OPTIONS, SCHEMA_UTILITY_METER_OPTIONS, SCHEMA_UTILITY_METER_TOGGLE +from custom_components.powercalc.flow_helper.schema import ( + SCHEMA_ENERGY_OPTIONS, + SCHEMA_UTILITY_METER_OPTIONS, + SCHEMA_UTILITY_METER_TOGGLE, +) if TYPE_CHECKING: from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow @@ -95,10 +99,16 @@ SCHEMA_GLOBAL_CONFIGURATION_THROTTLING = vol.Schema( vol.Optional(CONF_ENERGY_UPDATE_INTERVAL, default=DEFAULT_ENERGY_UPDATE_INTERVAL): selector.NumberSelector( selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX), ), - vol.Optional(CONF_GROUP_POWER_UPDATE_INTERVAL, default=DEFAULT_GROUP_POWER_UPDATE_INTERVAL): selector.NumberSelector( + vol.Optional( + CONF_GROUP_POWER_UPDATE_INTERVAL, + default=DEFAULT_GROUP_POWER_UPDATE_INTERVAL, + ): selector.NumberSelector( selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX), ), - vol.Optional(CONF_GROUP_ENERGY_UPDATE_INTERVAL, default=DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL): selector.NumberSelector( + vol.Optional( + CONF_GROUP_ENERGY_UPDATE_INTERVAL, + default=DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL, + ): selector.NumberSelector( selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX), ), }, @@ -141,7 +151,10 @@ class GlobalConfigurationFlow: def __init__(self, flow: PowercalcCommonFlow) -> None: self.flow = flow - async def async_step_global_configuration_discovery(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration_discovery( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """Handle the discovery configuration step.""" if user_input is not None: @@ -162,7 +175,10 @@ class GlobalConfigurationFlow: user_input, ) - async def async_step_global_configuration_throttling(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration_throttling( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """Handle the throttling related options.""" if user_input is not None: @@ -184,7 +200,10 @@ class GlobalConfigurationFlow: user_input, ) - async def async_step_global_configuration_energy(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration_energy( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """Handle the global configuration step.""" if user_input is not None: @@ -199,11 +218,18 @@ class GlobalConfigurationFlow: PowercalcFormStep( step=Step.GLOBAL_CONFIGURATION_ENERGY, schema=SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR, - form_kwarg={"description_placeholders": {"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/"}}, + form_kwarg={ + "description_placeholders": { + "docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/", + }, + }, ), ) - async def async_step_global_configuration_utility_meter(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration_utility_meter( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: """Handle the global configuration step.""" if user_input is not None: @@ -212,7 +238,7 @@ class GlobalConfigurationFlow: return self.flow.persist_config_entry() if not bool(self.flow.global_config.get(CONF_CREATE_UTILITY_METERS)) or user_input is not None: - return self.flow.async_create_entry( # type: ignore + return self.flow.async_create_entry( title="Global Configuration", data=self.flow.global_config, ) @@ -221,7 +247,11 @@ class GlobalConfigurationFlow: PowercalcFormStep( step=Step.GLOBAL_CONFIGURATION_UTILITY_METER, schema=SCHEMA_UTILITY_METER_OPTIONS, - form_kwarg={"description_placeholders": {"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/"}}, + form_kwarg={ + "description_placeholders": { + "docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/", + }, + }, ), ) @@ -231,7 +261,7 @@ class GlobalConfigurationConfigFlow(GlobalConfigurationFlow): super().__init__(flow) self.flow: PowercalcConfigFlow = flow - async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the global configuration step.""" get_global_powercalc_config(self.flow) await self.flow.async_set_unique_id(ENTRY_GLOBAL_CONFIG_UNIQUE_ID) @@ -272,7 +302,7 @@ class GlobalConfigurationOptionsFlow(GlobalConfigurationFlow): menu[Step.GLOBAL_CONFIGURATION_UTILITY_METER] = "Utility meter options" return menu - async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the global configuration step.""" if user_input is not None: diff --git a/custom_components/powercalc/flow_helper/flows/group.py b/custom_components/powercalc/flow_helper/flows/group.py index f48c1a3..ec47d49 100644 --- a/custom_components/powercalc/flow_helper/flows/group.py +++ b/custom_components/powercalc/flow_helper/flows/group.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable from typing import TYPE_CHECKING, Any from homeassistant.components.sensor import SensorDeviceClass @@ -15,7 +15,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError from homeassistant.helpers.selector import TextSelector @@ -318,7 +317,7 @@ class GroupFlow: def __init__(self, flow: PowercalcCommonFlow) -> None: self.flow = flow - async def async_step_assign_groups(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_assign_groups(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for assigning groups.""" group_entries = get_group_entries(self.flow.hass, GroupType.CUSTOM) if not group_entries: @@ -331,7 +330,7 @@ class GroupFlow: }, ) - async def _validate(user_input: dict[str, Any]) -> dict[str, Any]: + def _validate(user_input: dict[str, Any]) -> dict[str, Any]: groups = user_input.get(CONF_GROUP) or [] new_group = user_input.get(CONF_NEW_GROUP) if new_group: @@ -360,7 +359,7 @@ class GroupConfigFlow(GroupFlow): - name: str | None - selected_sensor_type: str | None - async_set_unique_id(), _abort_if_unique_id_configured() - - handle_form_step(PowercalcFormStep, user_input) -> FlowResult + - handle_form_step(PowercalcFormStep, user_input) -> ConfigFlowResult - async_show_menu(...), fill_schema_defaults(...), - create_group_selector(...), create_schema_group_custom(...) @@ -371,7 +370,7 @@ class GroupConfigFlow(GroupFlow): super().__init__(flow) self.flow: PowercalcConfigFlow = flow - async def async_step_menu_group(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + async def async_step_menu_group(self, _: dict[str, Any] | None = None) -> ConfigFlowResult: menu = [Step.GROUP_CUSTOM, Step.GROUP_DOMAIN, Step.GROUP_SUBTRACT, Step.GROUP_TRACKED_UNTRACKED] # Hide tracked/untracked if already present entry = self.flow.hass.config_entries.async_entry_for_domain_unique_id( @@ -387,9 +386,9 @@ class GroupConfigFlow(GroupFlow): group_type: GroupType, user_input: dict[str, Any] | None = None, schema: vol.Schema | None = None, - next_step: Callable[[dict[str, Any]], Coroutine[Any, Any, Step | None]] | None = None, - ) -> FlowResult: - async def _validate(ui: dict[str, Any]) -> dict[str, Any]: + next_step: Callable[[dict[str, Any]], Step | None] | None = None, + ) -> ConfigFlowResult: + def _validate(ui: dict[str, Any]) -> dict[str, Any]: if group_type == GroupType.CUSTOM: validate_group_input(ui) @@ -412,24 +411,28 @@ class GroupConfigFlow(GroupFlow): user_input, ) - async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: schema = SCHEMA_GROUP.extend(create_schema_group_custom(self.flow.hass).schema) return await self.handle_group_step(GroupType.CUSTOM, user_input, schema) - async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: return await self.handle_group_step(GroupType.DOMAIN, user_input) - async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: return await self.handle_group_step(GroupType.SUBTRACT, user_input) - async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: await self.flow.async_set_unique_id(UNIQUE_ID_TRACKED_UNTRACKED) self.flow.abort_if_unique_id_configured() if user_input is not None: user_input[CONF_NAME] = "Tracked / Untracked" - async def _next(ui: dict[str, Any]) -> Step | None: - return Step.GROUP_TRACKED_UNTRACKED_AUTO if bool(ui.get("group_tracked_auto", True)) else Step.GROUP_TRACKED_UNTRACKED_MANUAL + def _next(ui: dict[str, Any]) -> Step | None: + return ( + Step.GROUP_TRACKED_UNTRACKED_AUTO + if bool(ui.get("group_tracked_auto", True)) + else Step.GROUP_TRACKED_UNTRACKED_MANUAL + ) return await self.handle_group_step( GroupType.TRACKED_UNTRACKED, @@ -438,7 +441,10 @@ class GroupConfigFlow(GroupFlow): next_step=_next, ) - async def async_step_group_tracked_untracked_auto(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_tracked_untracked_auto( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: schema = await create_schema_tracked_untracked_auto(self.flow.hass) return await self.flow.handle_form_step( PowercalcFormStep( @@ -449,7 +455,10 @@ class GroupConfigFlow(GroupFlow): user_input, ) - async def async_step_group_tracked_untracked_manual(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_tracked_untracked_manual( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: schema = await create_schema_group_tracked_untracked_manual(self.flow.hass, user_input) return await self.flow.handle_form_step( PowercalcFormStep( @@ -468,21 +477,23 @@ class GroupOptionsFlow(GroupFlow): super().__init__(flow) self.flow: PowercalcOptionsFlow = flow - async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the group options flow.""" return await self.flow.async_handle_options_step( - user_input, create_schema_group_custom(self.flow.hass, self.flow.config_entry, True), Step.GROUP_CUSTOM + user_input, + create_schema_group_custom(self.flow.hass, self.flow.config_entry, True), + Step.GROUP_CUSTOM, ) - async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the group options flow.""" return await self.flow.async_handle_options_step(user_input, SCHEMA_GROUP_DOMAIN_OPTIONS, Step.GROUP_DOMAIN) - async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the group options flow.""" return await self.flow.async_handle_options_step(user_input, SCHEMA_GROUP_SUBTRACT_OPTIONS, Step.GROUP_SUBTRACT) - async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the group options flow.""" schema = SCHEMA_GROUP_TRACKED_UNTRACKED if self.flow.sensor_config.get(CONF_GROUP_TRACKED_AUTO, True): diff --git a/custom_components/powercalc/flow_helper/flows/library.py b/custom_components/powercalc/flow_helper/flows/library.py index 8f11a9d..72b2371 100644 --- a/custom_components/powercalc/flow_helper/flows/library.py +++ b/custom_components/powercalc/flow_helper/flows/library.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from homeassistant.config_entries import ConfigFlowResult -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector, translation import voluptuous as vol @@ -31,10 +30,23 @@ from custom_components.powercalc.discovery import ( ) from custom_components.powercalc.flow_helper.common import FlowType, PowercalcFormStep, Step from custom_components.powercalc.flow_helper.dynamic_field_builder import build_dynamic_field_schema -from custom_components.powercalc.flow_helper.schema import SCHEMA_ENERGY_SENSOR_TOGGLE, SCHEMA_UTILITY_METER_TOGGLE, build_sub_profile_schema -from custom_components.powercalc.helpers import collect_placeholders, iter_related_entity_placeholders, resolve_related_entity_placeholder +from custom_components.powercalc.flow_helper.schema import ( + SCHEMA_ENERGY_SENSOR_TOGGLE, + SCHEMA_UTILITY_METER_TOGGLE, + build_sub_profile_schema, +) +from custom_components.powercalc.helpers import ( + collect_placeholders, + iter_related_entity_placeholders, + resolve_related_entity_placeholder, +) from custom_components.powercalc.power_profile.library import ModelInfo, ProfileLibrary -from custom_components.powercalc.power_profile.power_profile import DEVICE_TYPE_DOMAIN, DOMAIN_DEVICE_TYPE_MAPPING, DiscoveryBy, PowerProfile +from custom_components.powercalc.power_profile.power_profile import ( + DEVICE_TYPE_DOMAIN, + DOMAIN_DEVICE_TYPE_MAPPING, + DiscoveryBy, + PowerProfile, +) if TYPE_CHECKING: from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow @@ -67,7 +79,7 @@ class LibraryFlow: async def async_step_manufacturer( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask the user to select the manufacturer.""" async def _create_schema() -> vol.Schema: @@ -82,7 +94,10 @@ class LibraryFlow: ] return vol.Schema( { - vol.Required(CONF_MANUFACTURER, default=self.flow.sensor_config.get(CONF_MANUFACTURER)): selector.SelectSelector( + vol.Required( + CONF_MANUFACTURER, + default=self.flow.sensor_config.get(CONF_MANUFACTURER), + ): selector.SelectSelector( selector.SelectSelectorConfig( options=manufacturers, mode=selector.SelectSelectorMode.DROPDOWN, @@ -104,7 +119,7 @@ class LibraryFlow: async def async_step_model( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask the user to select the model.""" def _build_model_label(model_id: str, model_name: str) -> str: @@ -139,10 +154,18 @@ class LibraryFlow: self._get_library_discovery_by(), ) ] - model = self.flow.selected_profile.model if self.flow.selected_profile else self.flow.sensor_config.get(CONF_MODEL) + model = ( + self.flow.selected_profile.model + if self.flow.selected_profile + else self.flow.sensor_config.get(CONF_MODEL) + ) return vol.Schema( { - vol.Required(CONF_MODEL, description={"suggested_value": model}, default=model): selector.SelectSelector( + vol.Required( + CONF_MODEL, + description={"suggested_value": model}, + default=model, + ): selector.SelectSelector( selector.SelectSelectorConfig( options=models, mode=selector.SelectSelectorMode.DROPDOWN, @@ -162,26 +185,29 @@ class LibraryFlow: user_input, ) - async def async_step_post_library( - self, - user_input: dict[str, Any] | None = None, - ) -> FlowResult: + async def async_step_post_library(self, _: dict[str, Any] | None = None) -> ConfigFlowResult: """ Handles the logic after the user either selected manufacturer/model himself or confirmed autodiscovered. Forwards to the next step in the flow. """ if not self.flow.selected_profile: - return self.flow.async_abort(reason="model_not_supported") # type:ignore # pragma: no cover + return self.flow.async_abort(reason="model_not_supported") # pragma: no cover if Step.LIBRARY_CUSTOM_FIELDS not in self.flow.handled_steps and self.flow.selected_profile.has_custom_fields: return await self.async_step_library_custom_fields() - if Step.AVAILABILITY_ENTITY not in self.flow.handled_steps and self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE: + if ( + Step.AVAILABILITY_ENTITY not in self.flow.handled_steps + and self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE + ): result = await self.async_step_availability_entity() if result: return result - if Step.SUB_PROFILE not in self.flow.handled_steps and await self.flow.selected_profile.requires_manual_sub_profile_selection: + if ( + Step.SUB_PROFILE not in self.flow.handled_steps + and await self.flow.selected_profile.requires_manual_sub_profile_selection + ): return await self.async_step_sub_profile() if ( @@ -191,21 +217,26 @@ class LibraryFlow: ): return await self.async_step_smart_switch() - if Step.FIXED not in self.flow.handled_steps and self.flow.selected_profile.needs_fixed_config: # pragma: no cover - return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_fixed() # type:ignore + if ( + Step.FIXED not in self.flow.handled_steps and self.flow.selected_profile.needs_fixed_config + ): # pragma: no cover + return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_fixed() # type: ignore[no-any-return] if Step.LINEAR not in self.flow.handled_steps and self.flow.selected_profile.needs_linear_config: - return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_linear() # type:ignore + return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_linear() # type: ignore[no-any-return] - if Step.MULTI_SWITCH not in self.flow.handled_steps and self.flow.selected_profile.calculation_strategy == CalculationStrategy.MULTI_SWITCH: - return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_multi_switch() # type:ignore + if ( + Step.MULTI_SWITCH not in self.flow.handled_steps + and self.flow.selected_profile.calculation_strategy == CalculationStrategy.MULTI_SWITCH + ): + return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_multi_switch() # type: ignore[no-any-return] - return await self.flow.flow_handlers[FlowType.GROUP].async_step_assign_groups() # type:ignore + return await self.flow.flow_handlers[FlowType.GROUP].async_step_assign_groups() # type: ignore[no-any-return] - async def async_step_library_custom_fields(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_library_custom_fields(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for custom fields.""" - async def _process_user_input(user_input: dict[str, Any]) -> dict[str, Any]: + def _process_user_input(user_input: dict[str, Any]) -> dict[str, Any]: return {CONF_VARIABLES: user_input} form_kwarg: dict[str, Any] | None = None @@ -234,11 +265,11 @@ class LibraryFlow: async def async_step_sub_profile( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the flow for sub profile selection.""" assert self.flow.selected_profile is not None - async def _validate(user_input: dict[str, Any]) -> dict[str, str]: + def _validate(user_input: dict[str, Any]) -> dict[str, str]: return {CONF_MODEL: f"{self.flow.sensor_config.get(CONF_MODEL)}/{user_input.get(CONF_SUB_PROFILE)}"} library = await ProfileLibrary.factory(self.flow.hass) @@ -254,7 +285,11 @@ class LibraryFlow: if remarks: remarks = "\n\n" + remarks - step = Step.SUB_PROFILE_PER_DEVICE if self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE else Step.SUB_PROFILE + step = ( + Step.SUB_PROFILE_PER_DEVICE + if self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE + else Step.SUB_PROFILE + ) return await self.flow.handle_form_step( PowercalcFormStep( @@ -275,16 +310,16 @@ class LibraryFlow: async def async_step_sub_profile_per_device( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: return await self.async_step_sub_profile(user_input) - async def async_step_smart_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_smart_switch(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Asks the user for the power of connect appliance for the smart switch.""" if self.flow.selected_profile and not self.flow.selected_profile.needs_fixed_config: return self.flow.persist_config_entry() - async def _validate(user_input: dict[str, Any]) -> dict[str, Any]: + def _validate(user_input: dict[str, Any]) -> dict[str, Any]: return { CONF_SELF_USAGE_INCLUDED: user_input.get(CONF_SELF_USAGE_INCLUDED), CONF_MODE: CalculationStrategy.FIXED, @@ -303,7 +338,7 @@ class LibraryFlow: user_input, ) - async def async_step_availability_entity(self, user_input: dict[str, Any] | None = None) -> FlowResult | None: + async def async_step_availability_entity(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult | None: """Handle the flow for availability entity.""" # Auto-resolve availability entity from profile placeholders auto_entity = self._resolve_availability_entity() @@ -377,7 +412,7 @@ class LibraryConfigFlow(LibraryFlow): async def async_step_library_multi_profile( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult | ConfigFlowResult: + ) -> ConfigFlowResult: """This step gets executed when multiple profiles are found for the source entity.""" if user_input is not None: selected_model: str = user_input.get(CONF_MODEL) # type: ignore @@ -426,7 +461,7 @@ class LibraryConfigFlow(LibraryFlow): async def async_step_library( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Try to autodiscover manufacturer/model first. Ask the user to confirm this or forward to manual library selection. """ @@ -439,7 +474,7 @@ class LibraryConfigFlow(LibraryFlow): return self._show_autodiscovered_profile_form() - async def _handle_library_confirmation(self, user_input: dict[str, Any]) -> FlowResult: + async def _handle_library_confirmation(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle the user's response to an autodiscovered library profile.""" if not user_input.get(CONF_CONFIRM_AUTODISCOVERED_MODEL) or not self.flow.selected_profile: return await self.async_step_manufacturer() @@ -454,27 +489,31 @@ class LibraryConfigFlow(LibraryFlow): async def _async_autodiscover_profile(self) -> None: """Populate the selected profile from the source entity when possible.""" - if not self.flow.source_entity or not self.flow.source_entity.entity_entry or self.flow.selected_profile is not None: + if ( + not self.flow.source_entity + or not self.flow.source_entity.entity_entry + or self.flow.selected_profile is not None + ): return self.flow.selected_profile = await get_power_profile_by_source_entity(self.flow.hass, self.flow.source_entity) if self.flow.selected_profile is None and self.flow.source_entity.device_entry: - self.flow.selected_profile = await get_power_profile_by_source_device(self.flow.hass, self.flow.source_entity) + self.flow.selected_profile = await get_power_profile_by_source_device( + self.flow.hass, + self.flow.source_entity, + ) - def _show_autodiscovered_profile_form(self) -> FlowResult: + def _show_autodiscovered_profile_form(self) -> ConfigFlowResult: """Show the confirmation form for an autodiscovered library profile.""" profile = self.flow.selected_profile assert profile is not None - return cast( - FlowResult, - self.flow.async_show_form( - step_id=Step.LIBRARY, - description_placeholders=self._build_library_description_placeholders(profile), - data_schema=SCHEMA_POWER_AUTODISCOVERED, - errors={}, - last_step=False, - ), + return self.flow.async_show_form( + step_id=Step.LIBRARY, + description_placeholders=self._build_library_description_placeholders(profile), + data_schema=SCHEMA_POWER_AUTODISCOVERED, + errors={}, + last_step=False, ) def _build_library_description_placeholders(self, profile: PowerProfile) -> dict[str, Any]: @@ -499,9 +538,19 @@ class LibraryConfigFlow(LibraryFlow): def _get_profile_source(self, profile: PowerProfile) -> str: """Build the autodiscovery source description.""" - translations = translation.async_get_cached_translations(self.flow.hass, self.flow.hass.config.language, "common", DOMAIN) - if profile.discovery_by == DiscoveryBy.DEVICE and self.flow.source_entity and self.flow.source_entity.device_entry: - return f"{translations.get(f'component.{DOMAIN}.common.source_device')}: {self.flow.source_entity.device_entry.name}" + translations = translation.async_get_cached_translations( + self.flow.hass, + self.flow.hass.config.language, + "common", + DOMAIN, + ) + if ( + profile.discovery_by == DiscoveryBy.DEVICE + and self.flow.source_entity + and self.flow.source_entity.device_entry + ): + label = translations.get(f"component.{DOMAIN}.common.source_device") + return f"{label}: {self.flow.source_entity.device_entry.name}" return f"{translations.get(f'component.{DOMAIN}.common.source_entity')}: {self.flow.source_entity_id}" @@ -537,7 +586,7 @@ class LibraryOptionsFlow(LibraryFlow): super().__init__(flow) self.flow: PowercalcOptionsFlow = flow - async def async_step_library_options(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_library_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" self.flow.is_library_flow = True self.flow.selected_sub_profile = self.flow.selected_profile.sub_profile # type: ignore diff --git a/custom_components/powercalc/flow_helper/flows/real_power.py b/custom_components/powercalc/flow_helper/flows/real_power.py index 6fdc7ea..9b75da4 100644 --- a/custom_components/powercalc/flow_helper/flows/real_power.py +++ b/custom_components/powercalc/flow_helper/flows/real_power.py @@ -5,8 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_ENTITY_ID, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector import voluptuous as vol @@ -39,7 +39,7 @@ class RealPowerConfigFlow: def __init__(self, flow: PowercalcConfigFlow) -> None: self.flow: PowercalcConfigFlow = flow - async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for real power sensor""" self.flow.selected_sensor_type = SensorType.REAL_POWER @@ -57,6 +57,6 @@ class RealPowerOptionsFlow: def __init__(self, flow: PowercalcOptionsFlow) -> None: self.flow: PowercalcOptionsFlow = flow - async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the real power options flow.""" return await self.flow.async_handle_options_step(user_input, SCHEMA_REAL_POWER_OPTIONS, Step.REAL_POWER) diff --git a/custom_components/powercalc/flow_helper/flows/virtual_power.py b/custom_components/powercalc/flow_helper/flows/virtual_power.py index 45801d8..cdff5ec 100644 --- a/custom_components/powercalc/flow_helper/flows/virtual_power.py +++ b/custom_components/powercalc/flow_helper/flows/virtual_power.py @@ -4,8 +4,8 @@ from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING, Any +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITIES, CONF_ENTITY_ID, CONF_ID, CONF_NAME, CONF_PATH, Platform -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError import voluptuous as vol @@ -17,6 +17,8 @@ from custom_components.powercalc.const import ( CONF_CALIBRATE, CONF_CREATE_ENERGY_SENSOR, CONF_CREATE_UTILITY_METERS, + CONF_FIXED, + CONF_FIXED_VALUE, CONF_GAMMA_CURVE, CONF_IGNORE_UNAVAILABLE_STATE, CONF_MAX_POWER, @@ -24,6 +26,7 @@ from custom_components.powercalc.const import ( CONF_MODE, CONF_MULTIPLY_FACTOR, CONF_MULTIPLY_FACTOR_STANDBY, + CONF_PLAYBOOK_ID, CONF_PLAYBOOKS, CONF_POWER, CONF_POWER_OFF, @@ -34,13 +37,25 @@ from custom_components.powercalc.const import ( CONF_STATE_TRIGGER, CONF_STATES_POWER, CONF_UNAVAILABLE_POWER, + CONF_VALUE, DUMMY_ENTITY_ID, CalculationStrategy, SensorType, ) -from custom_components.powercalc.flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults +from custom_components.powercalc.flow_helper.common import ( + FlowType, + PowercalcFormStep, + Step, + fill_schema_defaults, + unwrap_choose_selector, + wrap_choose_selector, +) from custom_components.powercalc.flow_helper.flows.global_configuration import get_global_powercalc_config -from custom_components.powercalc.flow_helper.flows.library import SCHEMA_POWER_OPTIONS_LIBRARY, SCHEMA_POWER_SMART_SWITCH +from custom_components.powercalc.flow_helper.flows.library import ( + SCHEMA_POWER_OPTIONS_LIBRARY, + SCHEMA_POWER_SMART_SWITCH, +) +from custom_components.powercalc.flow_helper.profile_preview import PREVIEW_NAME from custom_components.powercalc.flow_helper.schema import ( SCHEMA_ENERGY_SENSOR_TOGGLE, SCHEMA_SENSOR_ENERGY_OPTIONS, @@ -76,23 +91,125 @@ SCHEMA_POWER_OPTIONS = vol.Schema( }, ) +STATES_POWER_SELECTOR = selector.ObjectSelector( + selector.ObjectSelectorConfig( + fields={ + CONF_STATE: {"required": True, "selector": {"text": None}}, + CONF_POWER: {"required": True, "selector": {"number": {"mode": "box", "step": "any"}}}, + }, + multiple=True, + label_field=CONF_STATE, + description_field=CONF_POWER, + ), +) + +FIXED_CHOICE_SELECTORS: dict[str, selector.ChooseSelectorChoiceConfig] = { + CONF_POWER: {"selector": {"number": {"mode": "box", "step": "any"}}}, + CONF_POWER_TEMPLATE: {"selector": {"template": {}}}, + CONF_STATES_POWER: {"selector": STATES_POWER_SELECTOR.serialize()["selector"]}, +} + + +def order_choices_for_default( + choices: dict[str, selector.ChooseSelectorChoiceConfig], + default_choice: str | None, +) -> dict[str, selector.ChooseSelectorChoiceConfig]: + """Put the default choice first because HA initializes choose selectors from the first choice.""" + if default_choice not in choices: + return choices + return { + default_choice: choices[default_choice], + **{choice: config for choice, config in choices.items() if choice != default_choice}, + } + + +def find_present_choice(form_data: dict[str, Any], choices: dict[str, list[str] | str]) -> str | None: + """Find the first choice that has matching config data.""" + for choice_id, mapping in choices.items(): + keys = [mapping] if isinstance(mapping, str) else mapping + if any(key in form_data for key in keys): + return choice_id + return None + + SCHEMA_POWER_FIXED = vol.Schema( { - vol.Optional(CONF_POWER): vol.Coerce(float), - vol.Optional(CONF_POWER_TEMPLATE): selector.TemplateSelector(), - vol.Optional(CONF_STATES_POWER): selector.ObjectSelector(), + vol.Required(CONF_FIXED_VALUE): selector.ChooseSelector( + selector.ChooseSelectorConfig( + choices=FIXED_CHOICE_SELECTORS, + translation_key=CONF_FIXED_VALUE, + ), + ), }, ) SCHEMA_POWER_LINEAR = vol.Schema( { - vol.Optional(CONF_MIN_POWER): vol.Coerce(float), - vol.Optional(CONF_MAX_POWER): vol.Coerce(float), - vol.Optional(CONF_GAMMA_CURVE): vol.Coerce(float), - vol.Optional(CONF_CALIBRATE): selector.ObjectSelector(), + vol.Optional(CONF_MIN_POWER): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX, step="any"), + ), + vol.Optional(CONF_MAX_POWER): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX, step="any"), + ), + vol.Optional(CONF_GAMMA_CURVE): selector.NumberSelector( + selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX, step="any"), + ), + vol.Optional(CONF_CALIBRATE): selector.ObjectSelector( + selector.ObjectSelectorConfig( + fields={ + CONF_VALUE: {"required": True, "selector": {"number": {"mode": "box", "step": 1}}}, + CONF_POWER: {"required": True, "selector": {"number": {"mode": "box", "step": "any"}}}, + }, + multiple=True, + label_field=CONF_VALUE, + description_field=CONF_POWER, + ), + ), }, ) +FIXED_CHOICES: dict[str, list[str] | str] = { + CONF_STATES_POWER: CONF_STATES_POWER, + CONF_POWER_TEMPLATE: CONF_POWER_TEMPLATE, + CONF_POWER: CONF_POWER, +} + + +def fixed_choice_key_from_validated_value(value: object) -> str: + """Infer the fixed strategy config key from a validated ChooseSelector value.""" + if isinstance(value, list): + return CONF_STATES_POWER + if isinstance(value, str): + return CONF_POWER_TEMPLATE + return CONF_POWER + + +def unwrap_strategy_user_input(strategy: CalculationStrategy, user_input: dict[str, Any]) -> dict[str, Any]: + """Unwrap ChooseSelector wrappers and normalize list/dict shapes for strategy user input.""" + if strategy == CalculationStrategy.FIXED: + unwrap_choose_selector(user_input, CONF_FIXED_VALUE, fixed_choice_key_from_validated_value) + if CONF_STATE_TRIGGER in user_input and isinstance(user_input[CONF_STATE_TRIGGER], list): + user_input[CONF_STATE_TRIGGER] = { + item[CONF_STATE]: item[CONF_PLAYBOOK_ID] for item in user_input[CONF_STATE_TRIGGER] + } + return user_input + + +def wrap_strategy_form_data(strategy: CalculationStrategy, form_data: dict[str, Any]) -> dict[str, Any]: + """Wrap flat strategy config back into ChooseSelector form structure for display.""" + if strategy == CalculationStrategy.FIXED: + form_data = wrap_choose_selector(form_data, CONF_FIXED_VALUE, FIXED_CHOICES, raw_value=True) + if CONF_STATE_TRIGGER in form_data and isinstance(form_data[CONF_STATE_TRIGGER], dict): + form_data = { + **form_data, + CONF_STATE_TRIGGER: [ + {CONF_STATE: state, CONF_PLAYBOOK_ID: playbook_id} + for state, playbook_id in form_data[CONF_STATE_TRIGGER].items() + ], + } + return form_data + + SCHEMA_POWER_MULTI_SWITCH_MANUAL = vol.Schema( { vol.Required(CONF_POWER): vol.Coerce(float), @@ -127,6 +244,8 @@ STRATEGY_STEP_MAPPING: dict[CalculationStrategy, Step] = { CalculationStrategy.WLED: Step.WLED, } +STRATEGIES_WITHOUT_PREVIEW = {CalculationStrategy.PLAYBOOK, CalculationStrategy.MULTI_SWITCH} + class VirtualPowerFlow: def __init__(self, flow: PowercalcCommonFlow) -> None: @@ -139,13 +258,13 @@ class VirtualPowerFlow: create_schema_func = f"create_schema_{self.flow.strategy.lower()}" if hasattr(self, create_schema_func): - return await getattr(self, create_schema_func)() # type: ignore + return await getattr(self, create_schema_func)() # type: ignore[no-any-return] return STRATEGY_SCHEMAS[self.flow.strategy] async def create_schema_linear(self) -> vol.Schema: """Create the config schema for linear strategy.""" - return SCHEMA_POWER_LINEAR.extend( # type: ignore + return SCHEMA_POWER_LINEAR.extend( # type: ignore[no-any-return] { vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector( selector.AttributeSelectorConfig( @@ -156,6 +275,21 @@ class VirtualPowerFlow: }, ) + async def create_schema_fixed(self) -> vol.Schema: + """Create the config schema for fixed strategy.""" + fixed_config = self.flow.sensor_config.get(CONF_FIXED, {}) + default_choice = find_present_choice(fixed_config, FIXED_CHOICES) if isinstance(fixed_config, dict) else None + return vol.Schema( + { + vol.Required(CONF_FIXED_VALUE): selector.ChooseSelector( + selector.ChooseSelectorConfig( + choices=order_choices_for_default(FIXED_CHOICE_SELECTORS, default_choice), + translation_key=CONF_FIXED_VALUE, + ), + ), + }, + ) + async def create_schema_multi_switch(self) -> vol.Schema: """Create the config schema for multi switch strategy.""" @@ -180,31 +314,57 @@ class VirtualPowerFlow: async def create_schema_playbook(self) -> vol.Schema: """Create the config schema for playbook strategy.""" - base_path = Path(self.flow.hass.config.path("powercalc/playbooks")) - playbook_files = [str(p.relative_to(base_path)) for p in base_path.rglob("*") if p.is_file()] + + def _find_playbook_files() -> list[str]: + base_path = Path(self.flow.hass.config.path("powercalc/playbooks")) + return [str(p.relative_to(base_path)) for p in base_path.rglob("*") if p.is_file()] + + playbook_files = await self.flow.hass.async_add_executor_job(_find_playbook_files) + + state_trigger_state_selector: dict[str, Any] = {"text": None} + if self.flow.source_entity_id and self.flow.source_entity_id != DUMMY_ENTITY_ID: + state_trigger_state_selector = {"state": {"entity_id": self.flow.source_entity_id}} return vol.Schema( { vol.Optional(CONF_PLAYBOOKS): selector.ObjectSelector( - { - "multiple": True, - "description_field": CONF_PATH, - "label_field": CONF_ID, - "fields": { + selector.ObjectSelectorConfig( + fields={ CONF_ID: { "required": True, "selector": {"text": None}, }, CONF_PATH: { "required": True, - "selector": {"select": {"options": playbook_files, "mode": "dropdown", "custom_value": True}}, + "selector": { + "select": {"options": playbook_files, "mode": "dropdown", "custom_value": True}, + }, }, }, - }, + multiple=True, + description_field=CONF_PATH, + label_field=CONF_ID, + ), ), vol.Optional(CONF_REPEAT): selector.BooleanSelector(), vol.Optional(CONF_AUTOSTART): selector.TextSelector(), - vol.Optional(CONF_STATE_TRIGGER): selector.ObjectSelector(), + vol.Optional(CONF_STATE_TRIGGER): selector.ObjectSelector( + selector.ObjectSelectorConfig( + fields={ + CONF_STATE: { + "required": True, + "selector": state_trigger_state_selector, + }, + CONF_PLAYBOOK_ID: { + "required": True, + "selector": {"text": None}, + }, + }, + multiple=True, + description_field=CONF_PLAYBOOK_ID, + label_field=CONF_STATE, + ), + ), }, ) @@ -213,25 +373,25 @@ class VirtualPowerFlow: strategy: CalculationStrategy, user_input: dict[str, Any] | None = None, validate: Callable[[dict[str, Any]], None] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: self.flow.strategy = strategy async def _validate(user_input: dict[str, Any]) -> dict[str, Any]: + user_input = unwrap_strategy_user_input(strategy, user_input) if validate: validate(user_input) - # Convert states_power from dict to list to preserve order - if CONF_STATES_POWER in user_input and isinstance(user_input[CONF_STATES_POWER], dict): - user_input[CONF_STATES_POWER] = [{CONF_STATE: state, CONF_POWER: power} for state, power in user_input[CONF_STATES_POWER].items()] await self.flow.validate_strategy_config({strategy: user_input}) return {strategy: user_input} schema = await self.create_strategy_schema() - description_placeholders = {} - if strategy == CalculationStrategy.WLED: - description_placeholders = { - "docs_uri": "https://docs.powercalc.nl/strategies/wled/", - } + description_placeholders = { + "docs_uri": f"https://docs.powercalc.nl/strategies/{strategy.value.replace('_', '-')}/", + } + + form_kwarg: dict[str, Any] = {"description_placeholders": description_placeholders} + if strategy not in STRATEGIES_WITHOUT_PREVIEW: + form_kwarg["preview"] = PREVIEW_NAME return await self.flow.handle_form_step( PowercalcFormStep( @@ -239,12 +399,12 @@ class VirtualPowerFlow: schema=schema, next_step=Step.ASSIGN_GROUPS, validate_user_input=_validate, - form_kwarg={"description_placeholders": description_placeholders}, + form_kwarg=form_kwarg, ), user_input, ) - async def async_step_power_advanced(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_power_advanced(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for advanced options.""" if self.flow.is_options_flow: @@ -302,9 +462,9 @@ class VirtualPowerConfigFlow(VirtualPowerFlow): options_schema, get_global_powercalc_config(self.flow), ) - return schema.extend(power_options.schema) # type: ignore + return schema.extend(power_options.schema) # type: ignore[no-any-return] - async def async_step_virtual_power(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_virtual_power(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for virtual power sensor.""" errors: dict[str, str] = {} @@ -313,12 +473,16 @@ class VirtualPowerConfigFlow(VirtualPowerFlow): user_input.get(CONF_MODE) or CalculationStrategy.LUT, ) entity_id = user_input.get(CONF_ENTITY_ID) - if selected_strategy is not CalculationStrategy.PLAYBOOK and user_input.get(CONF_NAME) is None and entity_id is None: + if ( + selected_strategy is not CalculationStrategy.PLAYBOOK + and user_input.get(CONF_NAME) is None + and entity_id is None + ): errors[CONF_ENTITY_ID] = "entity_mandatory" if not errors: self.flow.source_entity_id = str(entity_id or DUMMY_ENTITY_ID) - self.flow.source_entity = await create_source_entity( + self.flow.source_entity = create_source_entity( self.flow.source_entity_id, self.flow.hass, ) @@ -329,33 +493,30 @@ class VirtualPowerConfigFlow(VirtualPowerFlow): return await self.forward_to_strategy_step(selected_strategy) - return self.flow.async_show_form( # type: ignore + return self.flow.async_show_form( step_id=Step.VIRTUAL_POWER, data_schema=self.create_schema_virtual_power(), - description_placeholders={ - "doc_uri_states_power": "https://docs.powercalc.nl/strategies/fixed/#power-per-state", - }, errors=errors, last_step=False, ) - async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for fixed sensor.""" return await self.handle_strategy_step(CalculationStrategy.FIXED, user_input) - async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for fixed sensor.""" return await self.handle_strategy_step(CalculationStrategy.LINEAR, user_input) - async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for multi switch strategy.""" return await self.handle_strategy_step(CalculationStrategy.MULTI_SWITCH, user_input) - async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for WLED sensor.""" return await self.handle_strategy_step(CalculationStrategy.WLED, user_input) - async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the flow for playbook sensor.""" def _validate(user_input: dict[str, Any]) -> None: @@ -364,13 +525,13 @@ class VirtualPowerConfigFlow(VirtualPowerFlow): return await self.handle_strategy_step(CalculationStrategy.PLAYBOOK, user_input, _validate) - async def forward_to_strategy_step(self, strategy: CalculationStrategy) -> FlowResult: + async def forward_to_strategy_step(self, strategy: CalculationStrategy) -> ConfigFlowResult: """Forward to the next step based on the selected strategy.""" step = STRATEGY_STEP_MAPPING.get(strategy) if step is None: - return await self.flow.flow_handlers[FlowType.LIBRARY].async_step_library() # type:ignore + return await self.flow.flow_handlers[FlowType.LIBRARY].async_step_library() # type: ignore[no-any-return] method = getattr(self.flow, f"async_step_{step}") - return await method() # type: ignore + return await method() # type: ignore[no-any-return] class VirtualPowerOptionsFlow(VirtualPowerFlow): @@ -383,40 +544,52 @@ class VirtualPowerOptionsFlow(VirtualPowerFlow): user_input: dict[str, Any], ) -> dict[str, Any]: """Build the config dict needed for the configured strategy.""" + if self.flow.strategy: + user_input = unwrap_strategy_user_input(self.flow.strategy, dict(user_input)) strategy_schema = await self.create_strategy_schema() strategy_options: dict[str, Any] = {} + flat_keys: set[str] = set() for key in strategy_schema.schema: + base_key = key.schema if isinstance(key, vol.Marker) else key + flat_keys.add(str(base_key)) + # The wrapper-key flat values land in user_input after unwrap; collect anything + # that matches a known strategy config key. + candidate_keys = flat_keys | { + CONF_POWER, + CONF_POWER_TEMPLATE, + CONF_STATES_POWER, + CONF_MIN_POWER, + CONF_MAX_POWER, + CONF_GAMMA_CURVE, + CONF_CALIBRATE, + } + for key in candidate_keys: if user_input.get(key) is None: continue - strategy_options[str(key)] = user_input.get(key) - # Convert states_power from dict to list to preserve order - if CONF_STATES_POWER in strategy_options and isinstance(strategy_options[CONF_STATES_POWER], dict): - strategy_options[CONF_STATES_POWER] = [ - {CONF_STATE: state, CONF_POWER: power} for state, power in strategy_options[CONF_STATES_POWER].items() - ] + strategy_options[key] = user_input[key] return strategy_options - async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_strategy_options_step(user_input) - async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_strategy_options_step(user_input) - async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_strategy_options_step(user_input) - async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_strategy_options_step(user_input) - async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the basic options flow.""" return await self.async_handle_strategy_options_step(user_input) - async def async_handle_strategy_options_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_handle_strategy_options_step(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: """Handle the option processing for the selected strategy.""" step = STRATEGY_STEP_MAPPING.get(self.flow.strategy or CalculationStrategy.FIXED, Step.FIXED) @@ -429,8 +602,13 @@ class VirtualPowerOptionsFlow(VirtualPowerFlow): **self.flow.sensor_config, **{k: v for k, v in strategy_options.items() if k not in self.flow.sensor_config}, } - # Convert states_power from list to dict for display in ObjectSelector - if CONF_STATES_POWER in merged_options and isinstance(merged_options[CONF_STATES_POWER], list): - merged_options[CONF_STATES_POWER] = {item[CONF_STATE]: item[CONF_POWER] for item in merged_options[CONF_STATES_POWER]} + if self.flow.strategy: + merged_options = wrap_strategy_form_data(self.flow.strategy, merged_options) schema = fill_schema_defaults(schema, merged_options) - return await self.flow.async_handle_options_step(user_input, schema, step) + form_kwarg = {"preview": PREVIEW_NAME} if self.flow.strategy not in STRATEGIES_WITHOUT_PREVIEW else None + return await self.flow.async_handle_options_step( + user_input, + schema, + step, + form_kwarg=form_kwarg, + ) diff --git a/custom_components/powercalc/flow_helper/profile_preview.py b/custom_components/powercalc/flow_helper/profile_preview.py new file mode 100644 index 0000000..e22dc0d --- /dev/null +++ b/custom_components/powercalc/flow_helper/profile_preview.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Protocol, cast + +from homeassistant.components import websocket_api +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import UnknownFlow +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType +import voluptuous as vol + +from custom_components.powercalc.common import SourceEntity +from custom_components.powercalc.const import CONF_FIXED_VALUE, CalculationStrategy +from custom_components.powercalc.errors import StrategyConfigurationError, UnsupportedStrategyError +from custom_components.powercalc.flow_helper.common import unwrap_choose_selector +from custom_components.powercalc.power_profile.power_profile import PowerProfile +from custom_components.powercalc.strategy.factory import PowerCalculatorStrategyFactory +from custom_components.powercalc.strategy.selector import detect_calculation_strategy + +PREVIEW_NAME = "powercalc" +PREVIEW_FRIENDLY_NAME = "Preview power" +PREVIEW_ICON = "mdi:flash" + + +class PreviewFlowProtocol(Protocol): + sensor_config: ConfigType + selected_profile: PowerProfile | None + source_entity: SourceEntity | None + + +async def async_setup_preview(hass: HomeAssistant) -> None: + """Set up the Powercalc preview websocket command.""" + websocket_api.async_register_command(hass, ws_start_preview) + + +@websocket_api.websocket_command( + { + vol.Required("type"): f"{PREVIEW_NAME}/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + }, +) +@websocket_api.async_response +async def ws_start_preview( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Generate a live Powercalc strategy preview.""" + flow = _get_flow_handler(hass, msg) + flow_status = _get_flow_status(hass, msg) + errors = _validate_user_input(flow_status.get("data_schema"), msg["user_input"]) + if errors: + connection.send_message( + { + "id": msg["id"], + "type": websocket_api.TYPE_RESULT, + "success": False, + "error": {"code": "invalid_user_input", "message": errors}, + }, + ) + return + + source_entity = flow.source_entity + if source_entity is None: + raise HomeAssistantError("No source entity available for Powercalc preview") + + preview = await build_profile_preview( + hass, + _build_preview_sensor_config(flow, flow_status["step_id"], msg["user_input"]), + source_entity, + flow.selected_profile, + ) + + connection.send_result(msg["id"]) + connection.send_message( + websocket_api.event_message( + msg["id"], + { + "attributes": preview["attributes"], + "state": preview["state"], + }, + ), + ) + connection.subscriptions[msg["id"]] = lambda: None + + +def _get_flow_handler(hass: HomeAssistant, msg: dict[str, Any]) -> PreviewFlowProtocol: + manager = hass.config_entries.flow if msg["flow_type"] == "config_flow" else hass.config_entries.options + try: + return cast(PreviewFlowProtocol, manager._progress[msg["flow_id"]]) # noqa: SLF001 + except KeyError as err: + raise UnknownFlow from err + + +def _get_flow_status(hass: HomeAssistant, msg: dict[str, Any]) -> dict[str, Any]: + manager = hass.config_entries.flow if msg["flow_type"] == "config_flow" else hass.config_entries.options + return cast(dict[str, Any], manager.async_get(msg["flow_id"])) + + +def _validate_user_input(schema: vol.Schema | None, user_input: dict[str, Any]) -> dict[str, str]: + if schema is None: + return {} + + errors: dict[str, str] = {} + key: vol.Marker + for key, validator in schema.schema.items(): + if key.schema not in user_input: + continue + try: + validator(user_input[key.schema]) + except vol.Invalid as ex: + errors[str(key.schema)] = str(ex.msg) + return errors + + +def _build_preview_sensor_config(flow: PreviewFlowProtocol, step_id: str, user_input: dict[str, Any]) -> ConfigType: + sensor_config = dict(flow.sensor_config) + try: + strategy = CalculationStrategy(step_id) + except ValueError: + return sensor_config + + sensor_config[strategy] = _unwrap_preview_strategy_input(strategy, user_input) + return sensor_config + + +def _unwrap_preview_strategy_input(strategy: CalculationStrategy, user_input: dict[str, Any]) -> dict[str, Any]: + """Unwrap form-only selector wrappers before building a preview strategy config.""" + unwrapped = dict(user_input) + if strategy == CalculationStrategy.FIXED: + unwrap_choose_selector(unwrapped, CONF_FIXED_VALUE) + return unwrapped + + +async def build_profile_preview( + hass: HomeAssistant, + sensor_config: ConfigType, + source_entity: SourceEntity, + power_profile: PowerProfile | None, +) -> dict[str, Any]: + """Build an entity-like preview containing only current calculated power.""" + current_power = await _calculate_current_power(hass, sensor_config, source_entity, power_profile) + return { + "attributes": { + ATTR_FRIENDLY_NAME: PREVIEW_FRIENDLY_NAME, + ATTR_ICON: PREVIEW_ICON, + }, + "state": _format_preview_state(current_power), + } + + +async def _calculate_current_power( + hass: HomeAssistant, + sensor_config: ConfigType, + source_entity: SourceEntity, + power_profile: PowerProfile | None, +) -> Decimal | None: + current_state = hass.states.get(source_entity.entity_id) + if current_state is None: + return None + + try: + cv.template_complex(sensor_config) + except vol.Invalid: + return None + + strategy = detect_calculation_strategy(sensor_config, power_profile) + try: + calculation_strategy = await PowerCalculatorStrategyFactory(hass).create( + sensor_config, + strategy, + power_profile, + source_entity, + ) + except (StrategyConfigurationError, UnsupportedStrategyError): + return None + + try: + return await calculation_strategy.calculate(current_state) + except HomeAssistantError: + return None + + +def _format_preview_state(power: Decimal | None) -> str: + if power is None: + return "unavailable" + return f"{_format_power(power)} W" + + +def _format_power(value: Decimal | float | str | None) -> str: + if value is None: + return "0" + decimal_value = Decimal(str(value)) + return f"{decimal_value:.2f}".rstrip("0").rstrip(".") diff --git a/custom_components/powercalc/group_include/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/group_include/__pycache__/__init__.cpython-314.pyc index ef4ed1606ae8c1a9d8c466dde247372a1f6ca232..e116fddfe7d9f88ee08a104e412211dfab10596a 100644 GIT binary patch delta 19 ZcmZ3$xPXycn~#@^0SHdN(4WXX6#ysL1hD`B delta 19 ZcmZ3$xPXycn~#@^0SI!IsZZpd3IHTD1PA~C diff --git a/custom_components/powercalc/group_include/__pycache__/filter.cpython-314.pyc b/custom_components/powercalc/group_include/__pycache__/filter.cpython-314.pyc index c7b2bdbdfbd0efee7b25aca3adfbb204ff37f1c5..ba336104912abd9990617c4d6292579bdac44146 100644 GIT binary patch delta 1424 zcmYk5drZ?;6vyxRm3GJ|lu{lo9U{KkKovzOU{RqykPVnnUIGGxg2nQ%#n&{5A#QPr zS=~X=#cfQNh{>{r++`+i8p$?iG;>rL#0@8#Zqa3A6o1N$Chq)xZkfM7KHu}Z_ndp~ zIrrYH(;x;wofM`HVf0rpGGUSMkDR0vb+ z-Yz^)cy#4xXi^}YC74Am4ln41L+D)iInih;S4k;n;&GSBO6v}qF2fPN$E*_I2=*l1 zfcLQ``2sQCl26kv%X&CLPDcFR(g!Db^P<@ToZ@e$r~yvm;UxhW#DlqeP?2gND?arM ze8xvp#|1dgdAb(p@1k`+&=P-U6v8Mj&pZTU_+94f#JsF@8rhp=ho2~c0dHoVfpI*% z{2OAGZ6Dmio3=^fmF#}FjqVjs;a7C8>?7}>RTXfLV&=mZgoWAodeny2(yA=Zeb~FmF+CPA4-nROjK$5)91n`)b=EV!}7(4TC!7T20c>*8=(+a#W z3r`meP;7DGZb1zg$hXOrM;7geFn+8kSs=N%K@VWWiQ*yRiIRL!<7CMmA>n`bPaVxL z2V+Wmi657C!aST)cAR*=EEf_G%DsXKaM)gqm&#vBGE-Z;iE?c-ELs^BkRV-GlHs&G zSuZ){QG9Q89Ivg|rBKO1Pw?leZ>{6zj4^Z3HFMGMs~g9v8~oJ`Bj(2I8mHgfcyxQ8 zzE@9%U*mkJ5Y%D!V_0m$qv=={(S$du-H^lA)&M{b=51=ynxu_H(nh34Qu3J;9NN?> zta(ARz9fTE8(;*fBog@n zl1lH7gm*RU4pPYhcK)UTkw2sly)Wqmu=}1 z+N3S#;gc=x(2j2R7erI*ZrH&;YV88pfpgmjp$&g%?-KU@*M(p_*0Bz{@s|!C(X(|M z^x)mC=ZU901+bSt@ElaY*Su%DUI>kVWM8t#NJJA3>|7G6gj8R($VfyD{GhDd{$*nnRBYhelDjh7v762%M7_58OErQF>2?N8~v-)Ld4=Lvg8HQ9pL8}PjCAx zpriT5pfO^jfbiQ=GY=r2yzhd$u3m!ByqmczdOi ztOqMUfI(p>=X)Nm2*>jyfllWN)IjAUtEyoPV+&8fI9@F5BraQ>PoAx-_29$))gG8Y z`JXVWL2~ z#8F7{t-Q(UZnIh(&26@3heHTPj*tYxj#;vt5`spfapW=gDI6@1!gtH&s{)nd%G)3l9#&2Q%)`m`PLN@3)c}?KQ`Nb6RT<}Arw#7bn}aYsZZ4Ighc$LwvTAuvKN-%ms`#f zTTJD!7%!QepvGi#f0hQQ+tk2#GhCFkn|^3UwtA+cd4`}dQX32iQ|3O2EKoIvFKqon zceUMDVjC;5jW)OY>@JVpCS)3TvdmylmZ`AB+7%)T zROy6sJ9*GuwfKsyW5w1{Tbr+Azo%orx7anl%j9vzQeI+KgHcK5(e>e`5}Kh+MqFrxJ% z;*HiW=n#y1dH_0br0p`Lbhh{KuHYVuI}G5)Lmj=uSo_n^jmPZQi0d6y;1bR_j!EEC zA#GngFIRxJS1WQPVlq~_vgI1_}>5-}2wx-@BVPzSv#kz*?6OChtjNaRSwa23=5 MD`qN|;4N3ezcw@9-T(jq diff --git a/custom_components/powercalc/group_include/__pycache__/include.cpython-314.pyc b/custom_components/powercalc/group_include/__pycache__/include.cpython-314.pyc index 5d81130019b5758f2028cdd6c6221c3e3d36b324..c42ef04f2413075ae990874d92e08aad94305fd1 100644 GIT binary patch delta 3785 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 list[entity_registry.RegistryEntry]: @@ -133,7 +133,11 @@ class GroupFilter(EntityFilter): filters = [] for single_group_id in group_ids: domain = split_entity_id(single_group_id)[0] - filter_instance = LightGroupFilter(hass, single_group_id) if domain == LIGHT_DOMAIN else StandardGroupFilter(hass, single_group_id) + filter_instance = ( + LightGroupFilter(hass, single_group_id) + if domain == LIGHT_DOMAIN + else StandardGroupFilter(hass, single_group_id) + ) filters.append(filter_instance) self.filter = CompositeFilter(filters, FilterOperator.OR) if len(filters) > 1 else filters[0] @@ -227,8 +231,7 @@ class WildcardFilter(EntityFilter): class TemplateFilter(EntityFilter): - def __init__(self, hass: HomeAssistant, template: Template) -> None: - template.hass = hass + def __init__(self, template: Template) -> None: self.entity_ids = template.async_render() def is_valid(self, entity: RegistryEntry) -> bool: @@ -311,7 +314,9 @@ class AreaFilter(EntityFilter): ) self.area_ids.append(area.id) - self.area_devices.update([device.id for device in device_registry.async_entries_for_area(device_reg, area.id)]) + self.area_devices.update( + [device.id for device in device_registry.async_entries_for_area(device_reg, area.id)], + ) def is_valid(self, entity: RegistryEntry) -> bool: return entity.area_id in self.area_ids or entity.device_id in self.area_devices @@ -350,7 +355,9 @@ class FloorFilter(EntityFilter): self.area_ids.extend([area.id for area in areas if area.id is not None]) for area in areas: - self.devices.extend([device.id for device in device_registry.async_entries_for_area(device_reg, area.id)]) + self.devices.extend( + [device.id for device in device_registry.async_entries_for_area(device_reg, area.id)], + ) def is_valid(self, entity: RegistryEntry) -> bool: return entity.area_id in self.area_ids or entity.device_id in self.devices diff --git a/custom_components/powercalc/group_include/include.py b/custom_components/powercalc/group_include/include.py index 39d8b93..cc732a1 100644 --- a/custom_components/powercalc/group_include/include.py +++ b/custom_components/powercalc/group_include/include.py @@ -5,6 +5,7 @@ from homeassistant.components import sensor from homeassistant.components.sensor import SensorDeviceClass from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import RegistryEntry from custom_components.powercalc.common import create_source_entity from custom_components.powercalc.const import ( @@ -52,7 +53,7 @@ async def find_entities( resolved_entities: list[Entity] = [] discoverable_entities: list[str] = [] - source_entities = await get_filtered_entity_list(hass, _build_filter(entity_filter)) + source_entities = get_filtered_entity_list(hass, _build_filter(entity_filter)) if _LOGGER.isEnabledFor(logging.DEBUG): # pragma: no cover _LOGGER.debug("Source entities: %s", [entity.entity_id for entity in source_entities]) @@ -70,29 +71,15 @@ async def find_entities( resolved_entities.append(existing) continue - is_real_sensor = False - - if source_entity.domain == sensor.DOMAIN: - if source_entity.platform != DOMAIN and not include_non_powercalc: - continue - - device_class = source_entity.device_class or source_entity.original_device_class - if device_class == SensorDeviceClass.POWER: - resolved_entities.append(RealPowerSensor(entity_id, source_entity.unit_of_measurement)) - is_real_sensor = True - elif device_class == SensorDeviceClass.ENERGY: - resolved_entities.append(RealEnergySensor(entity_id)) - is_real_sensor = True - - # No need to discover a profile for something we already resolved as a real sensor - if is_real_sensor: + if _should_skip_source_entity(source_entity, include_non_powercalc): continue - power_profile = await get_power_profile_by_source_entity( - hass, - await create_source_entity(entity_id, hass), - ) - if power_profile and not await power_profile.needs_user_configuration and power_profile.is_entity_domain_supported(source_entity): + real_sensor = _create_real_sensor(source_entity) + if real_sensor: + resolved_entities.append(real_sensor) + continue + + if await _is_discoverable_source_entity(hass, source_entity): discoverable_entities.append(entity_id) if exclude_utility_meters: @@ -105,6 +92,34 @@ async def find_entities( return FindEntitiesResult(resolved_entities, discoverable_entities) +def _should_skip_source_entity(source_entity: RegistryEntry, include_non_powercalc: bool) -> bool: + return source_entity.domain == sensor.DOMAIN and source_entity.platform != DOMAIN and not include_non_powercalc + + +def _create_real_sensor(source_entity: RegistryEntry) -> Entity | None: + if source_entity.domain != sensor.DOMAIN: + return None + + device_class = source_entity.device_class or source_entity.original_device_class + if device_class == SensorDeviceClass.POWER: + return RealPowerSensor(source_entity.entity_id, source_entity.unit_of_measurement) + if device_class == SensorDeviceClass.ENERGY: + return RealEnergySensor(source_entity.entity_id) + return None # pragma: no cover + + +async def _is_discoverable_source_entity(hass: HomeAssistant, source_entity: RegistryEntry) -> bool: + power_profile = await get_power_profile_by_source_entity( + hass, + create_source_entity(source_entity.entity_id, hass), + ) + return bool( + power_profile + and not await power_profile.needs_user_configuration + and power_profile.is_entity_domain_supported(source_entity), + ) + + def _build_filter(entity_filter: EntityFilter | None) -> EntityFilter: base_filter = CompositeFilter( [ @@ -112,7 +127,11 @@ def _build_filter(entity_filter: EntityFilter | None) -> EntityFilter: LambdaFilter(lambda entity: entity.platform != "utility_meter"), LambdaFilter(lambda entity: not str(entity.unique_id).startswith("powercalc_standby_group")), LambdaFilter(lambda entity: "tracked_" not in str(entity.unique_id)), - LambdaFilter(lambda entity: entity.platform != "tasmota" or not str(entity.entity_id).endswith(("_yesterday", "_today"))), + LambdaFilter( + lambda entity: ( + entity.platform != "tasmota" or not str(entity.entity_id).endswith(("_yesterday", "_today")) + ), + ), ], ) if not entity_filter: diff --git a/custom_components/powercalc/helpers.py b/custom_components/powercalc/helpers.py index 87334dd..f1e3126 100644 --- a/custom_components/powercalc/helpers.py +++ b/custom_components/powercalc/helpers.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) PLACEHOLDER_REGEX = re.compile(r"\[\[\s*([A-Za-z_]\w*(?::[A-Za-z_]\w*)*)\s*\]\]") -async def evaluate_power(power: Template | Decimal | float) -> Decimal | None: +def evaluate_power(power: Template | Decimal | float) -> Decimal | None: """When power is a template render it.""" if isinstance(power, Decimal): @@ -154,7 +154,10 @@ def collect_placeholders(data: list | str | dict[str, Any]) -> set[str]: return found -def replace_placeholders(data: list | str | dict[str, Any], replacements: dict[str, str]) -> list | str | dict[str, Any]: +def replace_placeholders( + data: list | str | dict[str, Any], + replacements: dict[str, str], +) -> list | str | dict[str, Any]: """Replace placeholders in a dictionary with values from a replacement dictionary.""" if isinstance(data, dict): for key, value in data.items(): @@ -304,7 +307,12 @@ def _get_related_entity_for_device( if matcher(entity_entry) ] if not related_entities: - _LOGGER.debug("No related entities found for device %s with %s %s", source_entity.device_entry.id, match_label, match_value) + _LOGGER.debug( + "No related entities found for device %s with %s %s", + source_entity.device_entry.id, + match_label, + match_value, + ) return None return related_entities[0] diff --git a/custom_components/powercalc/icons.json b/custom_components/powercalc/icons.json index 955473a..ff1be60 100644 --- a/custom_components/powercalc/icons.json +++ b/custom_components/powercalc/icons.json @@ -18,6 +18,9 @@ "get_group_entities": { "service": "mdi:format-list-group" }, + "debug_group": { + "service": "mdi:bug-outline" + }, "increase_daily_energy": { "service": "mdi:numeric" }, diff --git a/custom_components/powercalc/manifest.json b/custom_components/powercalc/manifest.json index 96b1b58..1b68bcd 100644 --- a/custom_components/powercalc/manifest.json +++ b/custom_components/powercalc/manifest.json @@ -22,5 +22,5 @@ "requirements": [ "numpy>=1.21.1" ], - "version": "v1.20.14" + "version": "v1.21.0" } \ No newline at end of file diff --git a/custom_components/powercalc/migrate.py b/custom_components/powercalc/migrate.py index 41a3031..8777426 100644 --- a/custom_components/powercalc/migrate.py +++ b/custom_components/powercalc/migrate.py @@ -1,7 +1,7 @@ from datetime import timedelta from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENABLED, CONF_ID, CONF_PATH +from homeassistant.const import CONF_ENABLED, CONF_ID, CONF_PATH, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.issue_registry import async_create_issue @@ -24,6 +24,7 @@ from custom_components.powercalc.const import ( CONF_PLAYBOOK, CONF_PLAYBOOKS, CONF_POWER, + CONF_POWER_SENSOR_CATEGORY, CONF_POWER_TEMPLATE, CONF_SENSOR_TYPE, CONF_STATE, @@ -41,43 +42,74 @@ async def async_migrate_config_entry(hass: HomeAssistant, config_entry: ConfigEn data = {**config_entry.data} if version <= 1: - conf_fixed = data.get(CONF_FIXED, {}) - if CONF_POWER in conf_fixed and CONF_POWER_TEMPLATE in conf_fixed: - conf_fixed.pop(CONF_POWER, None) + _migrate_power_template(data) if version <= 2 and data.get(CONF_SENSOR_TYPE) and CONF_CREATE_ENERGY_SENSOR not in data: data[CONF_CREATE_ENERGY_SENSOR] = True if version <= 3: - conf_playbook = data.get(CONF_PLAYBOOK, {}) - if CONF_STATES_TRIGGER in conf_playbook: - data[CONF_PLAYBOOK][CONF_STATE_TRIGGER] = conf_playbook.pop(CONF_STATES_TRIGGER) + _migrate_playbook_trigger(data) if version <= 4 and config_entry.entry_id == ENTRY_GLOBAL_CONFIG_UNIQUE_ID: - discovery_config = { - CONF_ENABLED: data.get(CONF_ENABLE_AUTODISCOVERY_DEPRECATED, True), - CONF_EXCLUDE_DEVICE_TYPES: data.get(CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED, []), - CONF_EXCLUDE_SELF_USAGE: data.get(CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED, False), - } - data[CONF_DISCOVERY] = discovery_config - for key in [ - CONF_ENABLE_AUTODISCOVERY_DEPRECATED, - CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED, - CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED, - ]: - data.pop(key, None) + _migrate_global_discovery_config(data) if version <= 5: - conf_playbook = data.get(CONF_PLAYBOOK, {}) - if CONF_PLAYBOOKS in conf_playbook: - data[CONF_PLAYBOOK][CONF_PLAYBOOKS] = [{CONF_ID: key, CONF_PATH: val} for key, val in conf_playbook.pop(CONF_PLAYBOOKS).items()] + _migrate_playbooks(data) if version <= 6: - conf_fixed = data.get(CONF_FIXED, {}) - if CONF_STATES_POWER in conf_fixed and isinstance(conf_fixed[CONF_STATES_POWER], dict): - data[CONF_FIXED][CONF_STATES_POWER] = [{CONF_STATE: key, CONF_POWER: val} for key, val in conf_fixed[CONF_STATES_POWER].items()] + _migrate_states_power(data) - hass.config_entries.async_update_entry(config_entry, data=data, version=7) + if version <= 7: + _migrate_invalid_power_sensor_category(data) + + hass.config_entries.async_update_entry(config_entry, data=data, version=8) + + +def _migrate_power_template(data: dict) -> None: + conf_fixed = data.get(CONF_FIXED, {}) + if CONF_POWER in conf_fixed and CONF_POWER_TEMPLATE in conf_fixed: + conf_fixed.pop(CONF_POWER, None) + + +def _migrate_playbook_trigger(data: dict) -> None: + conf_playbook = data.get(CONF_PLAYBOOK, {}) + if CONF_STATES_TRIGGER in conf_playbook: + data[CONF_PLAYBOOK][CONF_STATE_TRIGGER] = conf_playbook.pop(CONF_STATES_TRIGGER) + + +def _migrate_global_discovery_config(data: dict) -> None: + data[CONF_DISCOVERY] = { + CONF_ENABLED: data.get(CONF_ENABLE_AUTODISCOVERY_DEPRECATED, True), + CONF_EXCLUDE_DEVICE_TYPES: data.get(CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED, []), + CONF_EXCLUDE_SELF_USAGE: data.get(CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED, False), + } + for key in [ + CONF_ENABLE_AUTODISCOVERY_DEPRECATED, + CONF_DISCOVERY_EXCLUDE_DEVICE_TYPES_DEPRECATED, + CONF_DISCOVERY_EXCLUDE_SELF_USAGE_DEPRECATED, + ]: + data.pop(key, None) + + +def _migrate_playbooks(data: dict) -> None: + conf_playbook = data.get(CONF_PLAYBOOK, {}) + if CONF_PLAYBOOKS in conf_playbook: + data[CONF_PLAYBOOK][CONF_PLAYBOOKS] = [ + {CONF_ID: key, CONF_PATH: val} for key, val in conf_playbook.pop(CONF_PLAYBOOKS).items() + ] + + +def _migrate_states_power(data: dict) -> None: + conf_fixed = data.get(CONF_FIXED, {}) + if CONF_STATES_POWER in conf_fixed and isinstance(conf_fixed[CONF_STATES_POWER], dict): + data[CONF_FIXED][CONF_STATES_POWER] = [ + {CONF_STATE: key, CONF_POWER: val} for key, val in conf_fixed[CONF_STATES_POWER].items() + ] + + +def _migrate_invalid_power_sensor_category(data: dict) -> None: + if data.get(CONF_POWER_SENSOR_CATEGORY) == EntityCategory.CONFIG: + data.pop(CONF_POWER_SENSOR_CATEGORY) async def async_fix_legacy_profile_config_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: @@ -113,7 +145,7 @@ async def async_fix_legacy_profile_config_entry(hass: HomeAssistant, config_entr ) -async def handle_legacy_discovery_config(hass: HomeAssistant, global_config: dict, yaml_config: dict) -> None: +def handle_legacy_discovery_config(hass: HomeAssistant, global_config: dict, yaml_config: dict) -> None: """Handle legacy discovery config. Might be removed in future Powercalc version""" discovery_options = global_config.setdefault(CONF_DISCOVERY, {}) deprecated_map = { @@ -150,7 +182,7 @@ async def handle_legacy_discovery_config(hass: HomeAssistant, global_config: dic ) -async def handle_legacy_update_interval_config(hass: HomeAssistant, global_config: dict, yaml_config: dict) -> None: +def handle_legacy_update_interval_config(hass: HomeAssistant, global_config: dict, yaml_config: dict) -> None: """Handle legacy group update interval config. Might be removed in future Powercalc version""" has_legacy_config = False diff --git a/custom_components/powercalc/power_profile/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/power_profile/__pycache__/__init__.cpython-314.pyc index bd8b56348afa6d078b7163849b1328a68179c2f1..f12bb99a66dc73c5156f1de8c68bca86a7dd8f34 100644 GIT binary patch delta 19 ZcmZ3$xPXycn~#@^0SHdN(4WXX6#ysL1hD`B delta 19 ZcmZ3$xPXycn~#@^0SI!IsZZpd3IHTD1PA~C diff --git a/custom_components/powercalc/power_profile/__pycache__/error.cpython-314.pyc b/custom_components/powercalc/power_profile/__pycache__/error.cpython-314.pyc index dd42c866032a065bc3ae81601411a480bfa7f447..e96c49fe50c2c366897dd6b523ebeba99cdda084 100644 GIT binary patch delta 20 acmaFF{)nAhn~#@^0SHdN(BH^?of!Z)GzF#r delta 20 acmaFF{)nAhn~#@^0SI!Isc+=I&I|xE=LG!# diff --git a/custom_components/powercalc/power_profile/__pycache__/factory.cpython-314.pyc b/custom_components/powercalc/power_profile/__pycache__/factory.cpython-314.pyc index d97a8e1e3dd4e0fa43cdb4a0d24ac1937802bc10..bc9f88511fd7faffde511e36d2f5df0adef4c6aa 100644 GIT binary patch delta 20 acmbOwIZKjTn~#@^0SHdN(BH`2&jSE2g#`Nm delta 20 acmbOwIZKjTn~#@^0SI!Isc+=&=K%mKI0RAv diff --git a/custom_components/powercalc/power_profile/__pycache__/library.cpython-314.pyc b/custom_components/powercalc/power_profile/__pycache__/library.cpython-314.pyc index 7e31ebfd5ecac402fec3ce6a5b7e1a78558be529..bae791bd694c497ebbbae890dd871dfef3af6505 100644 GIT binary patch delta 997 zcmXYwUuaWT9LLY^=H8_D=H@o0>CH`>q_=;P+C*$q!(z<38e`mSyE>}`nGecvLPtl( zN;@DnlDirpTP)6>9u(FqJh;g7!0+}Gag*>6fAhoEc(qqc-@yNQvqJ)GJ|cifga zDI_>#dpW5jV%k1V8c}qo-J~~qqVO;Kx#tSd;kOkbBhjzS5dENc0xq*Vy+;6^)7O2s zFIcK>sbNcvSZb%GMmN3gu+kvz4rR&H@Dnv;1;X{fu3BK%?UU<)TwTpo)!atAzN9(5 zMBnQuIMWFb-@d;W4&wzb6~e_aScwF=jzBRhuB5%3#|i?yANB7;u0)|E ziZm1vN{f>9pYmmAwTv2SGlQPPpN|zQx9O3y-F<#Xmh)RkgM}( zMz^Ceq_9I!HuRV+p<^_`KHI~ojdzaBNg;~icn$XBa25Q`^3R&(m{ezsUQJIWpBNKs z`TB|Z+KKsf4<=+tq!FZ~=3&J4NVbU2pn;zYdK(2k2qO_!9wbt1t)Y5x`3%w=W?}xIty`uXJ15dKDugMfXrH4QtY zW3ZLr;bo{ZnUL^U4V^}&7NAU*r{7VqCLRn2AK=6U9|xhBa4}g@O?&WwUWhRLTO%cBd_brI!;bPFRZDp;W}Wh5b2P|sjwLvF8b zXXHSVVr1sy;X)7mCa(%FgSAuctlKTr3HZ7<2Osopgo|X%w*}x6hT@UI(K4;0TRVCt zk$W~NpB=KyWhf~*FDaML$mJvZ5^^|c3v0IUY^E~gFtU--U`S}$IWRz}a&G@CnR%f^jm_XmrS_Y~01LuXj$ z?jBh9&WX8_;S`3PeGnD-$)eSi<8Fvr`M75(V}C(Z z5~nyj)67abGPMq(PJXIMp!`MzrdCrWOH{_)p{MYIILl9R)Gk%HV9i^WQO#mTSwfzH zsEn8D%%%rS(-ArbE!=R}ol1J(^RG7=U>9WuCkj(8le52sj&U;vP+dMSkKW#UmiQtX-0C(}@P1_WDf94zL$@$p|S;7wZ>8DZZeyp_n s1-hAe8AsYbuTLaVaG1h{>=7>cZtm39) z7&bE_D35`m@-?Sm0;J>_!UzgPmtiE~E#phnAG#1Hbfa@41Qhjl_s2Q8_qq3+``mjv z2k_AV3@)QVFNyJEe|AmqxZ#&EsN&mWZUI%K8jfKjHyZaphD~fwy$fujWb;+D@GbLf zpoKClpJ4}`wS0~3R5yMzKBoBiFF~~X$uRbCvXTQ`n~<)ZN#|#nslwWfebi^|!(KWw zL!sX$G~jcpw6&w1khTqnsU__n91^v3>af?~C?}`a14rn6$7!5kUq(65N&B8^$7#-= zXo0RPq3sgdCfbl$kFz|OsfYZnLRP;=#uw8Orc8! z2QiJ`D0~yhrrW+Qc&MXj5YN(qQY#%>P>npcywEFS7TsAG!$Q7MoDRG=S~Zn4!Oy!& zl7LdN@Gi=DqF*DUjGT+2sGx_7;sWn1eg`YLrpyQgY4b~;VlA6rZUENM&hm>0abQU? z@ERRix(jveU6zxGN8R$Hj)p`diuju?(tXLQ%^FHT#2Zi0sRA+Mf{Hc`8tB(R{p7fW zVkkG1wEeCqAhFBiJ0V9)h1$cqWUWllfbJspo`zGX@p4fqjgtgt++%ykFSM%S_Ij) zF@{E`WSx3laYq&R6~!~6k?rzNis!v>)W1w!zDiwPb=_Zm*1-~ z^+gNAO}?n&`75zsl~O*;9hPLs_wxXMJ(5|jn3yN!5)eT6YI=`?Lak@&1ouZDGDbs-=Xk9n?usQ=K zY;MX;V4H3egT#Fq@H){2v$=%fA7=a-*mTR5%ovsp0gH7Zn=acczCX@+@4UQo?|tvS zFW8!zH*3eDP&W{S@0z;SAQ@^!RwvMC#y7HJ*KqpP_F+LqdglXN@vHcn7a zu8D5Vu0tD@nor{tVNL@&=*XOhI3xGcsNGV5&p0`)7WkCjO8*MX1#>q6=jo$)r*Vn% zGmOx5$Gz>+TTgrE*WwC~%};{TBdBeIiS*VQd`s_I|G?GBXw9}J^szZ}4e%Y6Km8&4 zDZ&0HzNb<9n;7Kk1qNV%8W%R;C!W42AGk@LXS$VJf>x}0g3Zq+0l$$gs|mj|XC(qd zbUC{n!(5uv2@F%RqX#j%?zoG4GTa$V(93yIiN(%FJmjC9BftadThfiG9Dc4B$`sJG zB{r&Bb{2XzF24dz3S>2#sjEa!V=H=*Mi*8-fQ35qk0OJYuaY6>k=xaTOzL<22OIUf z+pviDtbPMnNW%q}kwfik#*j^?Hl)(I!d-B(aqWPDJi4=P1Z(-``ZS<$a#!y;1P`}( z9tU2K!eMwhqbNavmu%h$w$MM`sKh(P2jJ(5jXI!|_WMp?2OD0j1GZE1rt8?v{>|%w zO8UH{1^d{wB|F|8p{-w}*Tf4k$+tNuCWI-`Iv$5kS2RwS+;YY4ZSN(ZhJN+eW?oyY z`buv20uf(8-B_lURjYgI)arV*{(w3o97|AAbWx#bb@!*zp@0in)Ej7(t@Tpb3FP4>JV5wgfrUmNk j#RI}Wp^+H{5fhX2cPu@AE;wCYW1l6Z(5~srnJMvq|8Z1t diff --git a/custom_components/powercalc/power_profile/__pycache__/sub_profile_selector.cpython-314.pyc b/custom_components/powercalc/power_profile/__pycache__/sub_profile_selector.cpython-314.pyc index 908f0062244b8bf4dc051eaf63f3f75230d4e1b0..832c272a1ce449577b6dc5881747024bbc83bf13 100644 GIT binary patch literal 18625 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 diff --git a/custom_components/powercalc/power_profile/library.py b/custom_components/powercalc/power_profile/library.py index c7aec88..f144628 100644 --- a/custom_components/powercalc/power_profile/library.py +++ b/custom_components/powercalc/power_profile/library.py @@ -141,7 +141,13 @@ class ProfileLibrary: raise LibraryError(f"Model {model_info.manufacturer} {model_info.model} not found") model_info = next(iter(models)) - profile = await self.create_power_profile(model_info, source_entity, custom_directory, variables, process_variables) + profile = await self.create_power_profile( + model_info, + source_entity, + custom_directory, + variables, + process_variables, + ) if sub_profile: await profile.select_sub_profile(sub_profile) @@ -163,7 +169,11 @@ class ProfileLibrary: if linked_profile := json_data.get("linked_profile", json_data.get("linked_lut")): linked_manufacturer, linked_model = linked_profile.split("/") - linked_json_data, directory = await self._load_model_data(linked_manufacturer, linked_model, custom_directory) + linked_json_data, directory = await self._load_model_data( + linked_manufacturer, + linked_model, + custom_directory, + ) json_data.update(linked_json_data) raw_sub_profiles = await self._hass.async_add_executor_job(load_sub_profile_data, directory) @@ -202,7 +212,12 @@ class ProfileLibrary: replacements = self.compute_replacement_variables(placeholders, variables.copy(), source_entity) return cast(dict[str, Any], replace_placeholders(json_data, replacements)) - def compute_replacement_variables(self, placeholders: set[str], variables: dict[str, str], source_entity: SourceEntity | None) -> dict[str, str]: + def compute_replacement_variables( + self, + placeholders: set[str], + variables: dict[str, str], + source_entity: SourceEntity | None, + ) -> dict[str, str]: variables = variables or {} if source_entity: @@ -216,7 +231,9 @@ class ProfileLibrary: source_entity=source_entity, ) if not related_entity: - raise LibraryError(build_related_entity_placeholder_not_found_message(placeholder, source_entity.entity_id)) + raise LibraryError( + build_related_entity_placeholder_not_found_message(placeholder, source_entity.entity_id), + ) variables[placeholder] = related_entity return variables @@ -286,7 +303,9 @@ class ProfileLibrary: async def _load_model_data(self, manufacturer: str, model: str, custom_directory: str | None) -> tuple[dict, str]: """Load the model data from the appropriate directory.""" - loader = LocalLoader(self._hass, custom_directory, is_custom_directory=True) if custom_directory else self._loader + loader = ( + LocalLoader(self._hass, custom_directory, is_custom_directory=True) if custom_directory else self._loader + ) result = await loader.load_model(manufacturer, model) if not result: raise LibraryError(f"Model {manufacturer} {model} not found") diff --git a/custom_components/powercalc/power_profile/loader/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/power_profile/loader/__pycache__/__init__.cpython-314.pyc index a640e4bfc300884385f94f4d82e17f5f7371d601..d697a6e0a9fa081480f104251c236ab59ade14ab 100644 GIT binary patch delta 19 ZcmZ3^xSWw&n~#@^0SHdN(4WXX2LLGB1jYaW delta 19 ZcmZ3^xSWw&n~#@^0SI!IsZZpd0{|sA1RVeX diff --git a/custom_components/powercalc/power_profile/loader/__pycache__/composite.cpython-314.pyc b/custom_components/powercalc/power_profile/loader/__pycache__/composite.cpython-314.pyc index bbf91685962ed4d2fb945e1e389b0b1a98141442..a320c28132ba6eb97d33a2330b3fe870058f2dee 100644 GIT binary patch delta 899 zcmZXTO=uHA6vyXfXLpm$CMIi5yBpiIjk^tPMB5%(L7^*cQKBBCA1Bc$h}wd|A|ePa zdQcBy?D|zN_TmRv4-ya&{D|PG2u*8AdXn4}JZu^<=AypYwkmcG??3b3H}KwX-+XO< z)b4Nd`y_&E_Vcz=)cef;o@;aQMu}jU-r~p^1wS7HmbAJk>Lo(lJB>N=mJUz5ChF89j2~r4YcSdiHUg<9=b$KP4Q=$uso|pA` zSub;>Ho7G1)iD{9FGf4y0c;l@dOI>P-Ua)EXpZ$__%T5$#LG)17K$Nts`b|C$8<3i zw|V9Yq_%mwcQF*RMRs%($#-?|M&}fl?!I<-(NkaYplw|c!f+d_Z?;|v66lr~Xt8E% ze6axO=dl|)L`S%UxY9_XWkt_&DVpM(rZ!pi;+t`&q=D7WY=wj)_v82C-*_63ikguY z3PwC{^yQ4cx1$S2GT)e-Z%n4;GEaPJu`yXF52W%KNS zjI_VlL}t1FVDqq}v<{mWZR>*^hPw_nuTK(<6;hVw>(m)Feu)2v8vb%N21t!h(N$_G z-^X#Mw&68uHS4KG^n9c%7wLK-&kY<|h$QoBa$ZfQc?M1?st!2ie{kA5a1u;vCKYDD zbjeSHxO+`a39^z4aG#}uDOm;obLh@o-_Qdna{&>cCsPQXM6q^n`~-V5%{96UWlv}a z>>6Y^FXIeXT);MPe1Vg0`ku5XumCc!VwJHP?0MYdE K?}VxItbPL=qu?k2 delta 993 zcmZXTUr1AN6vxlGyZ7$ib=~HlHm56PQ~RTsBC?91E>?HMJNiips839m10j3 zk=rdy=wbGd{{maX3<_iqKK0@>S#M!)K@XcoYcJ9H-I)db9?tjtzW4Y0-QW4#bEoUS z)f=mgc?O_y^~HhyxFKp>(N&|+oCXS!3QYzJ=holHiX0;yDbMY0=PW3O`371A3v8OKWk{Vw*htuzzNrI2gW|63+1Z_&NO8x@WYo4?V zP}b&#{{Ze`ey*vA*6xrgqoz`}A5-FhGIdd@5-J!@^^4M=f$jPSY}GDUA;|B5J)ZY5 zck2su1Z$*}87dD=`=9$6SRXg#e_ECFb|<{ut37>$>lU8kh|@`IWqg}k8D5I7y21is zq%0&%`59cF2A2U~uEy3!Y#AY9D`D8?;lz3LfU@_cnbP+>nQ=f$)LqSf5L^Q;!DMk36oct@S@7azFP(cak2GPO zMv5J$veC4SovO9lU!hgiv{Y*H!;)Q9k*umM)U_O6J8~N}Y26<+mZi9f6shOTT@212 z+!1`kotZOp=FBn{%z_q zX(rK2nk0HzlSOaV%+O0+mQJg-5cG|T<<>5DXNs26nX09BrfF$~WJu~Q!(*OoMPw+w zks4!KMg}P&ep?pt+lO3@QergIZz>@^$36V?QQbNjWLhK`UAQ%$D}!VZR2?E3^%GDp zRg*G_Li~B0(Q@kHZwve#je7&6f%>zvNYv)f0Zh@{{#-33K(ti98Me>oy{2>2!vAi% zK-2h3(!HD%IF6+vqyeZhKp8Za&0m&FV9ihDQaKL>`9$fizD0?-ap@L-o<-s_u}u8T z0#Ho=#c`I6133t}0QjZi^nc^z5s}P?=mUY5N{yc7@x$gnTMBT!m-{S@&SI26OQ8x8 zOF7Nv3zqxT%}-j3_T#xevo5J&joDEKUsN}1Q2is6VSksN-4A7Er(6*-rD9e&{?$ThVyBMGXg8=$J4f=! z%dy}@*k?5qf2^TUPmSMPr5@8L05`s#9hY;t(^OW~yjalHX9una7TYaWMJO4DRpsK%djR)3P zq627Y95$JuajDzvAkz?EEYKfr;6L74!v}Mc+}40~6c%`3VF?LX`LEJ4_?a9>sx4sK zFmCHj`x{MCg!)WN|IRs0clP)k#yc9J2~y05fY;K~ zhNDVwWH>aXYRZK1M&kKd4^Q@#Cfhf>XfJ=pQ_fF#O57N)#>0uXBuT*v4Ny$1d-^{LC$*y}h|9VQtL&;R_dPGcRt`%qTse`K)l3u`n zkw5GygrUz>eCAi%wzfOy)pFWyldo2^6}3BfeZhF=x4fJz}yGGnscv~xO=rqC`Of(&42A_K430~a6yNYV*Yka0?-wPs2 zFlbm0ipjC?j3&16**4fuD18JMSSF9Lpc)ooUp~Xz*COmayrMi7yq;N z1(*PCdpcHpop+Sf1GCJOoGdIyL1}{ycsGLzMPA4*ZKkjD!=?U%nCO^TzA5dV!97qH z5l{8z%m)PE(un9--qLDmkJdu7QHK?=gOB163h^1cjK2%Ttt4n6|5C|8C)o$YTj&qT3&U_X_qj{W5+Q(PdL*w zZajm!S*9jOf?9YjDTovI@#pGuATx~B*U-!S`}IH16Ffy>J^o_DY>EN?aDe|!&f{lI zc0Ql!m?wSohzWkNml{mfnAj8T$*(#IN(|X35!Rnhgq*xi!7Kp63=56TFa9T3)voRY z6nx)VuEX-chDZc!OT>w{S>{ z&U!;iY&0@9#lj?%loDb6Zcj@3>RX zPM@AW`^xE8&MPv_$~?&f7%GgO#u+XkN%k%xDAA_M#%o_LGj-Bt}D=-ggR zThShd8zZF&Uj|+QCV+GT23ccIJWrRgoJ`PXqGpNnSVJDY#ouV?%NH-<#a%?E#kls- z11ul=H2y`yj$I`@$MB*z|vM&?RgD delta 3048 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 Qt2MY_r1&bLy^?wV1;JTGkpKVy delta 2502 zcmZ`*dr*|u6~E`(2m65Cg>PA2i!2Y7_iIH|BDk>7pn?nHqKIs8d9C6u?zi~nWlYni zaRg5?j1#9NQ>UZpbfWXwhfUH3BWamT>28!z8S7(e&2*g3ESiW;I&JUW#h5?ZJHzjH z?mg$8d+t5oIm0njB4(iqO&fvz2@1sxE2%tbp5iThRnLXjYa{^e6?$#lI;V zb#`KgMq?c5RWv43)ZrGDTho~BOR^K6u`wc@WaWu}Ro&uX0)HFT%-I1iHR`bLHO+Qg{Tup8<~YD+$K%K&4@;~|}U zX)S0;#iVHE()yZcJ*46gpN~hQcPv@FS*g_Zsasz-R)rkbV$m`lcg6hug(F2xH6eWw zt<1wX7V8tF&7-z>c3-T~$HQ`T=r3b@tWV}$Nuw0sjLinPioZ6bsLz+p<&+0<%C8$M z#0Q3-a@87`fI6A<-P}42Oh!SSLOQ7_VsZu4sijv*m5ffNlaV+v%F8OZ-K~TGe{BOtnBL+%qJc$$r~&UOH>Y4a(T6oW!L~SZ58H z!3Z{05LMIy82Zy67y7;_2eF8mUfo&s0Aec}E-bBjJmfLBC_~oWm1v zc>H~b{6~)6a130J89XJN65jZ~+BxmuX|pksdI}P$QuXW+kz%I#1Ncb1Z%zUDP?VOi zWQYZ;-{W9XoL}=6zzxx}?g_wkvFoJ-Xu3xO7YbPP!(@?2WR=`P2rq06hWJYQ73W8p zVR_OwT{U$$toP>AJx&RQ=Ocm&uM!gZcm`+I+aihx*>APUSf%L7V83%MRd-W+BiWRQ z4dr}_ni_0tgxtjE4hX52Y>Fa8NtTgKW_+#uQ~6!06MWcEkveoUyYMSQbd}tN6%+D5 zv`)5u(&jJOda*UIs_iYczt_7+$Ud-e!7%xqJ)<4t>QVJVcs{1Su;Ni@J2g;Na?c)z zCeH)|J>htoWjUOSv!uu;NfN^(6+RoCfvLw5#*t=-{p;sk0gzpT@p!(B};l* zv39U2S+7WVv8HG~w5FSobW$R5L6#Kfwq!?ox6v5A=@hbPGqPcNDDa(h&OYcb)p0lZ zxci3q@$Rwiarc;eqUE}w5bxMFXrBqJ-5xKpB09JimKAs#n(|)=;9h# zGb|8>SA<}Qr_Esww6!k@9&cFiA3Pd1#XwCG{7$qsdV!x{A5EX4m8B{)X!BwH6O4MC z=~5=LuERz*4cSQkmznb02S9VvTD383Pl#<99&9QB6MA$;oNoFMV(|Oh>^a^e)WCa` z!7&Q+a-pZ+=jxr8d-wUgot@rSn5LaVmJ&7gwJL$N`4w#7KKoi-==u@#4cPMqrIGk6 z`#ae(Wi#C3W5-L!O2^B`$|tht_`J{gyzegM&hZ7G^96XaxkRgG_Wu%Ss>pGuPR2lU zMtL$^&4tQBay37Mh3e`=$<;E8vN~3BO_s;77_24oYi1*rr?m3wWSEMTQKP9u33V}( z5Qnd}xS&}4XUi#o5^t)Lsu|XxaS`YQ;RK6QCByIv&9@JkeR>u4DOUp@;IXlj#=-g-e;gDZX-enxA(1 zw>Ae_+5(Q%@)&Ap!R-`5IAroaCDH?f`zlP%032G z?rTC0LN%j4O4PGx>iRK9PyT(6?R%kH&5JPnO1)jubKE0(+IcL04Q03>XU(sUU?n=w?7j5y|cK&gIp1UjDSPCmS z+BTA^SSYpJMnJmWHR+6bI+iSQJE-cSe*QQUJ|4leQZ{)Yg1928!mlRH8K zz+o)vpVmF4LCz~W-5q@%m)qx!L)C$FNXGmF^>pvRfrk=!U348O<=}OE_t-sd1>ncq zGhj@-Ft7(86ZJz@`v1(1p^Y$x=Z41PlD$PNF~tnb3|29S!p7n4`2O%FScl&Xuhu{R zv3o2b^5`H(r@=(59(g&M_5GzqCI(e_cBIrm+f=9-Ehe(+4Oi%*3I8*ap`!T-C227O z^N#z{p3u|g<-WsxE_b&#f*q}3!0N)wdW1Jc{N(s7z%o2@q6jMSvlBmwcFMp!y7~!W I9Qrl+FY{c>y#N3J diff --git a/custom_components/powercalc/power_profile/loader/composite.py b/custom_components/powercalc/power_profile/loader/composite.py index 90b8dd6..94e0ecd 100644 --- a/custom_components/powercalc/power_profile/loader/composite.py +++ b/custom_components/powercalc/power_profile/loader/composite.py @@ -11,7 +11,8 @@ class CompositeLoader(Loader): self.loaders = loaders async def initialize(self) -> None: - [await loader.initialize() for loader in self.loaders] # type: ignore[func-returns-value] + for loader in self.loaders: + await loader.initialize() async def get_manufacturer_listing( self, @@ -20,7 +21,11 @@ class CompositeLoader(Loader): ) -> set[tuple[str, str]]: """Get listing of available manufacturers.""" - return {manufacturer for loader in self.loaders for manufacturer in await loader.get_manufacturer_listing(device_types, discovery_by)} + return { + manufacturer + for loader in self.loaders + for manufacturer in await loader.get_manufacturer_listing(device_types, discovery_by) + } async def find_manufacturers(self, search: str) -> set[str]: """Check if a manufacturer is available. Also must check aliases.""" @@ -42,7 +47,11 @@ class CompositeLoader(Loader): ) -> set[tuple[str, str]]: """Get listing of available models and display names for a given manufacturer.""" - return {model for loader in self.loaders for model in await loader.get_model_listing(manufacturer, device_types, discovery_by)} + return { + model + for loader in self.loaders + for model in await loader.get_model_listing(manufacturer, device_types, discovery_by) + } async def load_model(self, manufacturer: str, model: str) -> tuple[dict, str] | None: for loader in self.loaders: diff --git a/custom_components/powercalc/power_profile/loader/local.py b/custom_components/powercalc/power_profile/loader/local.py index 42286b4..cbfe7da 100644 --- a/custom_components/powercalc/power_profile/loader/local.py +++ b/custom_components/powercalc/power_profile/loader/local.py @@ -1,3 +1,4 @@ +from functools import partial import json import logging import os @@ -101,12 +102,9 @@ class LocalLoader(Loader): _model = model.lower() if self._is_custom_directory: - model_path = os.path.join(self._data_directory) - model_json_path = os.path.join(model_path, "model.json") - if not os.path.exists(model_json_path): - raise LibraryLoadingError(f"model.json not found for manufacturer {_manufacturer} " + f"and model {_model} in path {model_json_path}") - - model_json = await self._hass.async_add_executor_job(self._load_json, model_json_path) + model_path, model_json = await self._hass.async_add_executor_job( + partial(self._load_custom_model, _manufacturer, _model), + ) return model_json, model_path lib_models = self._manufacturer_model_listing.get(_manufacturer) @@ -205,6 +203,17 @@ class LocalLoader(Loader): self._manufacturer_model_listing[manufacturer].update({search_key: profile}) + def _load_custom_model(self, manufacturer: str, model: str) -> tuple[str, dict[str, Any]]: + """Load model.json from a directly configured custom model directory.""" + model_path = os.path.join(self._data_directory) + model_json_path = os.path.join(model_path, "model.json") + if not os.path.exists(model_json_path): + raise LibraryLoadingError( + f"model.json not found for manufacturer {manufacturer} and model {model} in path {model_json_path}", + ) + + return model_path, self._load_json(model_json_path) + def _load_json(self, model_json_path: str) -> dict[str, Any]: """Load model.json file for a given model.""" with open(model_json_path) as file: diff --git a/custom_components/powercalc/power_profile/loader/remote.py b/custom_components/powercalc/power_profile/loader/remote.py index 2e4d105..48bfe00 100644 --- a/custom_components/powercalc/power_profile/loader/remote.py +++ b/custom_components/powercalc/power_profile/loader/remote.py @@ -64,7 +64,7 @@ class RemoteLoader(Loader): """Initialize the loader.""" integration = await async_get_integration(self.hass, DOMAIN) - powercalc_version = AwesomeVersion(integration.version) + powercalc_version = AwesomeVersion(str(integration.version)) self.library_contents = await self.load_library_json() self.profile_hashes = await self.hass.async_add_executor_job(self._load_profile_hashes) @@ -138,7 +138,7 @@ class RemoteLoader(Loader): async def _download_remote_library_json() -> dict[str, Any] | None: """ Download library.json from Github. - If download is successful, save it to local storage to use as fallback in case of internet connection issues. + On success, save it to local storage as a fallback for internet connection issues. """ _LOGGER.debug("Loading library.json from github") @@ -182,7 +182,10 @@ class RemoteLoader(Loader): return { (manufacturer["dir_name"], manufacturer["full_name"]) for manufacturer in self.library_contents.get("manufacturers", []) - if any(self._model_matches_filters(model, device_types, discovery_by) for model in manufacturer.get("models", [])) + if any( + self._model_matches_filters(model, device_types, discovery_by) + for model in manufacturer.get("models", []) + ) } @async_cache @@ -225,7 +228,12 @@ class RemoteLoader(Loader): async def find_model(self, manufacturer: str, search: set[str]) -> list[str]: """Find matching model IDs in the library.""" models = self.model_lookup.get(manufacturer, {}) - return [model["id"] for phrase in search if (phrase_lower := phrase.lower()) in models for model in models[phrase_lower]] + return [ + model["id"] + for phrase in search + if (phrase_lower := phrase.lower()) in models + for model in models[phrase_lower] + ] @async_cache async def find_model_migration(self, manufacturer: str, model: str) -> str | None: @@ -274,12 +282,19 @@ class RemoteLoader(Loader): raise LibraryLoadingError("Model not found in library: %s/%s", manufacturer, model) return model_info - async def _needs_update(self, model_info: LibraryModel, manufacturer: str, model: str, model_path: str, force_update: bool) -> bool: + async def _needs_update( + self, + model_info: LibraryModel, + manufacturer: str, + model: str, + model_path: str, + force_update: bool, + ) -> bool: """Check if the model needs to be updated.""" if force_update: return True - path_exists = os.path.exists(model_path) + path_exists = await self.hass.async_add_executor_job(os.path.exists, model_path) if not path_exists: return True @@ -287,7 +302,13 @@ class RemoteLoader(Loader): new_hash = model_info.get("hash") return existing_hash != new_hash - async def _download_profile_with_retry(self, manufacturer: str, model: str, storage_path: str, model_path: str) -> None: + async def _download_profile_with_retry( + self, + manufacturer: str, + model: str, + storage_path: str, + model_path: str, + ) -> None: """Attempt to download the profile, with retry logic and error handling.""" try: model_info = self._get_library_model(manufacturer, model) @@ -297,12 +318,22 @@ class RemoteLoader(Loader): self.profile_hashes[f"{manufacturer}/{model}"] = model_hash await self.hass.async_add_executor_job(self._write_profile_hashes, self.profile_hashes) except ProfileDownloadError as e: - if not os.path.exists(model_path): - if os.path.exists(storage_path): + path_exists, storage_path_exists = await self.hass.async_add_executor_job( + self._profile_paths_exist, + model_path, + storage_path, + ) + if not path_exists: + if storage_path_exists: await self.hass.async_add_executor_job(shutil.rmtree, storage_path) # pragma: no cover raise e _LOGGER.debug("Failed to download profile, falling back to local profile") + @staticmethod + def _profile_paths_exist(model_path: str, storage_path: str) -> tuple[bool, bool]: + """Check profile paths from the executor.""" + return os.path.exists(model_path), os.path.exists(storage_path) + async def _load_model_json(self, model_path: str) -> dict: """Load the JSON data from the model file.""" @@ -330,7 +361,10 @@ class RemoteLoader(Loader): """Retrieve the storage path for a given manufacturer and model.""" return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, manufacturer, model)) - async def download_with_retry(self, callback: Callable[[], Coroutine[Any, Any, None | dict[str, Any]]]) -> None | dict[str, Any]: + async def download_with_retry( + self, + callback: Callable[[], Coroutine[Any, Any, None | dict[str, Any]]], + ) -> None | dict[str, Any]: """Download a file from a remote endpoint with retries""" max_retries = 3 retry_count = 0 @@ -342,7 +376,9 @@ class RemoteLoader(Loader): _LOGGER.debug(e) retry_count += 1 if retry_count == max_retries: - raise ProfileDownloadError(f"Failed to download even after {max_retries} retries, falling back to local copy") from e + raise ProfileDownloadError( + f"Failed to download even after {max_retries} retries, falling back to local copy", + ) from e await asyncio.sleep(self.retry_timeout) _LOGGER.warning("Failed to download, retrying... (Attempt %d of %d)", retry_count + 1, max_retries) @@ -396,7 +432,7 @@ class RemoteLoader(Loader): return {} with open(path) as f: - return json.load(f) # type: ignore + return json.load(f) # type: ignore[no-any-return] def _write_profile_hashes(self, hashes: dict[str, str]) -> None: """Write profile hashes to local storage""" diff --git a/custom_components/powercalc/power_profile/power_profile.py b/custom_components/powercalc/power_profile/power_profile.py index 1fc8197..15e4d53 100644 --- a/custom_components/powercalc/power_profile/power_profile.py +++ b/custom_components/powercalc/power_profile/power_profile.py @@ -95,7 +95,9 @@ DEVICE_TYPE_DOMAIN: dict[DeviceType, str | set[str]] = { DeviceType.UPS: SENSOR_DOMAIN, } -SUPPORTED_DOMAINS: set[str] = {domain for domains in DEVICE_TYPE_DOMAIN.values() for domain in (domains if isinstance(domains, set) else {domains})} +SUPPORTED_DOMAINS: set[str] = { + domain for domains in DEVICE_TYPE_DOMAIN.values() for domain in (domains if isinstance(domains, set) else {domains}) +} def _build_domain_device_type_mapping() -> Mapping[str, set[DeviceType]]: @@ -393,7 +395,8 @@ class PowerProfile: if found_profile is None: raise ModelNotSupportedError( - f"Sub profile not found (manufacturer: {self._manufacturer}, model: {self._model}, sub_profile: {sub_profile})", + f"Sub profile not found (manufacturer: {self._manufacturer}, " + f"model: {self._model}, sub_profile: {sub_profile})", ) self._sub_profile_dir = os.path.join(self._directory, sub_profile) diff --git a/custom_components/powercalc/power_profile/sub_profile_selector.py b/custom_components/powercalc/power_profile/sub_profile_selector.py index a9ed69f..8944afe 100644 --- a/custom_components/powercalc/power_profile/sub_profile_selector.py +++ b/custom_components/powercalc/power_profile/sub_profile_selector.py @@ -2,7 +2,7 @@ from __future__ import annotations from enum import StrEnum import re -from typing import Any, NamedTuple, Protocol +from typing import NamedTuple, Protocol from homeassistant.core import HomeAssistant, State @@ -79,7 +79,13 @@ class SubProfileSelectConfig(NamedTuple): class SubProfileMatcher(Protocol): @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> SubProfileMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> SubProfileMatcher: """Create a matcher from a config dict.""" def match(self, entity_state: State, source_entity: SourceEntity) -> str | None: @@ -114,8 +120,15 @@ class EntityStateMatcher(SubProfileMatcher): return self._mapping.get(state.state) @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> EntityStateMatcher: # noqa: ANN401 - return cls(kwargs["hass"], kwargs["source_entity"], config["entity_id"], config["map"]) + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> EntityStateMatcher: + assert hass is not None + return cls(hass, source_entity, config["entity_id"], config["map"]) def get_tracking_entities(self) -> list[str]: return [self._entity_id] @@ -134,7 +147,13 @@ class AttributeMatcher(SubProfileMatcher): return self._mapping.get(val) @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> AttributeMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> AttributeMatcher: return cls(config["attribute"], config["map"]) def get_tracking_entities(self) -> list[str]: @@ -153,7 +172,13 @@ class EntityIdMatcher(SubProfileMatcher): return None @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> EntityIdMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> EntityIdMatcher: return cls(config["pattern"], config["profile"]) def get_tracking_entities(self) -> list[str]: @@ -176,7 +201,13 @@ class IntegrationMatcher(SubProfileMatcher): return None @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> IntegrationMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> IntegrationMatcher: return cls(config["integration"], config["profile"]) def get_tracking_entities(self) -> list[str]: @@ -204,7 +235,13 @@ class EntityRegistryMatcher(SubProfileMatcher): return None @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> EntityRegistryMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> EntityRegistryMatcher: return cls(config["property"], config["value"], config["profile"]) def get_tracking_entities(self) -> list[str]: @@ -236,7 +273,13 @@ class ModelIdMatcher(SubProfileMatcher): return None @classmethod - def from_config(cls, config: dict, **kwargs: Any) -> ModelIdMatcher: # noqa: ANN401 + def from_config( + cls, + config: dict, + *, + hass: HomeAssistant | None = None, + source_entity: SourceEntity | None = None, + ) -> ModelIdMatcher: return cls(config["model_id"], config["profile"]) def get_tracking_entities(self) -> list[str]: diff --git a/custom_components/powercalc/repairs.py b/custom_components/powercalc/repairs.py index 981fa41..3e1ebe0 100644 --- a/custom_components/powercalc/repairs.py +++ b/custom_components/powercalc/repairs.py @@ -19,10 +19,7 @@ class SubProfileRepairFlow(RepairsFlow): self._config_entry = config_entry self._hass = hass - async def async_step_init( - self, - user_input: dict[str, str] | None = None, - ) -> data_entry_flow.FlowResult: + async def async_step_init(self, _: dict[str, str] | None = None) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" return await self.async_step_sub_profile() @@ -32,9 +29,9 @@ class SubProfileRepairFlow(RepairsFlow): new_data = self._config_entry.data.copy() new_data[CONF_MODEL] = f"{new_data[CONF_MODEL]}/{user_input[CONF_SUB_PROFILE]}" self._hass.config_entries.async_update_entry(self._config_entry, data=new_data) - return self.async_create_entry(title="", data={}) + return self.async_create_entry(title="", data={}) # type: ignore[no-any-return] - source_entity = await create_source_entity(self._config_entry.data[CONF_ENTITY_ID], self._hass) + source_entity = create_source_entity(self._config_entry.data[CONF_ENTITY_ID], self._hass) profile = await get_power_profile(self.hass, dict(self._config_entry.data), source_entity) assert profile sub_profile_schema = await build_sub_profile_schema(profile, None) @@ -43,7 +40,7 @@ class SubProfileRepairFlow(RepairsFlow): if remarks: remarks = "\n\n" + remarks - return self.async_show_form( + return self.async_show_form( # type: ignore[no-any-return] step_id="sub_profile", data_schema=sub_profile_schema, description_placeholders={ @@ -57,7 +54,7 @@ class SubProfileRepairFlow(RepairsFlow): async def async_create_fix_flow( hass: HomeAssistant, - issue_id: str, + _: str, data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" diff --git a/custom_components/powercalc/select.py b/custom_components/powercalc/select.py index 31a4a5b..7c741da 100644 --- a/custom_components/powercalc/select.py +++ b/custom_components/powercalc/select.py @@ -47,9 +47,9 @@ def delayed_add_entities_handler( async def async_setup_platform( hass: HomeAssistant, - config: ConfigType, + _config: ConfigType, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + _discovery_info: DiscoveryInfoType | None = None, ) -> None: """Setup sensors from YAML config sensor entries.""" key = _key(None) diff --git a/custom_components/powercalc/sensor.py b/custom_components/powercalc/sensor.py index 7978712..c5a4638 100644 --- a/custom_components/powercalc/sensor.py +++ b/custom_components/powercalc/sensor.py @@ -130,6 +130,7 @@ from .const import ( SERVICE_ACTIVATE_PLAYBOOK, SERVICE_CALIBRATE_ENERGY, SERVICE_CALIBRATE_UTILITY_METER, + SERVICE_DEBUG_GROUP, SERVICE_GET_ACTIVE_PLAYBOOK, SERVICE_GET_GROUP_ENTITIES, SERVICE_INCREASE_DAILY_ENERGY, @@ -410,7 +411,7 @@ def _register_entity_id_change_listener( """ @callback - async def _entity_rename_listener(event: Event) -> None: + def _entity_rename_listener(event: Event) -> None: """Handle renaming of the entity""" old_entity_id = event.data["old_entity_id"] new_entity_id = event.data[CONF_ENTITY_ID] @@ -429,12 +430,11 @@ def _register_entity_id_change_listener( """Only dispatch the listener for update events concerning the source entity""" # Breaking change in 2024.4.0, check for Event for versions prior to this - if type(event) is Event: # Intentionally avoid `isinstance` because it's slow and we trust `Event` is not subclassed - event = event.data # pragma: no cover + event_data = event.data if isinstance(event, Event) else event return ( - event["action"] == "update" # type: ignore - and "old_entity_id" in event # type: ignore - and event["old_entity_id"] == source_entity_id # type: ignore + event_data["action"] == "update" + and "old_entity_id" in event_data + and event_data["old_entity_id"] == source_entity_id ) hass.bus.async_listen( @@ -548,6 +548,13 @@ def register_entity_services() -> None: supports_response=SupportsResponse.ONLY, ) + platform.async_register_entity_service( + SERVICE_DEBUG_GROUP, + {}, + "debug_group", + supports_response=SupportsResponse.ONLY, + ) + def convert_config_entry_to_sensor_config(config_entry: ConfigEntry, hass: HomeAssistant) -> ConfigType: # noqa: C901 """Convert the config entry structure to the sensor config used to create the entities.""" @@ -572,7 +579,9 @@ def convert_config_entry_to_sensor_config(config_entry: ConfigEntry, hass: HomeA """Convert on_time dictionary to timedelta.""" on_time = config.get(CONF_ON_TIME) config[CONF_ON_TIME] = ( - timedelta(hours=on_time["hours"], minutes=on_time["minutes"], seconds=on_time["seconds"]) if on_time else timedelta(days=1) + timedelta(hours=on_time["hours"], minutes=on_time["minutes"], seconds=on_time["seconds"]) + if on_time + else timedelta(days=1) ) def process_states_power(states_power: dict | list) -> dict: @@ -582,7 +591,10 @@ def convert_config_entry_to_sensor_config(config_entry: ConfigEntry, hass: HomeA """ if isinstance(states_power, list): states_power = {item[CONF_STATE]: item[CONF_POWER] for item in states_power} - return {key: Template(value, hass) if isinstance(value, str) and "{{" in value else value for key, value in states_power.items()} + return { + key: Template(value, hass) if isinstance(value, str) and "{{" in value else value + for key, value in states_power.items() + } def process_daily_fixed_energy() -> None: """Process daily fixed energy configuration.""" @@ -868,20 +880,15 @@ async def create_individual_sensors( ) -> EntitiesBucket: """Create entities (power, energy, utility meters) which track the appliance.""" - source_entity = await create_source_entity(sensor_config[CONF_ENTITY_ID], hass) + source_entity = create_source_entity(sensor_config[CONF_ENTITY_ID], hass) # For device-based profiles, attach the device entry to the source entity - if source_entity.entity_id == DUMMY_ENTITY_ID and "device" in sensor_config: - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get(sensor_config["device"]) - if device_entry: - source_entity = source_entity._replace(device_entry=device_entry) + source_entity = _attach_configured_device_entry(hass, sensor_config, source_entity) - if (used_unique_ids := hass.data[DOMAIN].get(DATA_USED_UNIQUE_IDS)) is None: - used_unique_ids = hass.data[DOMAIN][DATA_USED_UNIQUE_IDS] = [] # pragma: no cover + used_unique_ids = hass.data[DOMAIN].get(DATA_USED_UNIQUE_IDS, []) try: - await check_entity_not_already_configured( + check_entity_not_already_configured( sensor_config, source_entity, hass, @@ -905,17 +912,17 @@ async def create_individual_sensors( except PowercalcSetupError: return EntitiesBucket() entities_to_add.append(power_sensor) - energy_sensor = await create_energy_sensor_if_needed(hass, sensor_config, power_sensor, source_entity) + energy_sensor = create_energy_sensor_if_needed(hass, sensor_config, power_sensor, source_entity) if energy_sensor: entities_to_add.append(energy_sensor) attach_energy_sensor_to_power_sensor(power_sensor, energy_sensor) if energy_sensor: - entities_to_add.extend(await create_utility_meters(hass, energy_sensor, sensor_config, config_entry)) + entities_to_add.extend(create_utility_meters(hass, energy_sensor, sensor_config, config_entry)) await attach_entities_to_source_device(config_entry, entities_to_add, hass, source_entity) - update_registries(hass, source_entity, entities_to_add, context) + unique_id = sensor_config.get(CONF_UNIQUE_ID) or source_entity.unique_id if unique_id: used_unique_ids.append(unique_id) @@ -925,6 +932,21 @@ async def create_individual_sensors( return EntitiesBucket(new=entities_to_add, existing=[]) +def _attach_configured_device_entry( + hass: HomeAssistant, + sensor_config: dict, + source_entity: SourceEntity, +) -> SourceEntity: + if source_entity.entity_id != DUMMY_ENTITY_ID or "device" not in sensor_config: + return source_entity + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(sensor_config["device"]) + if device_entry: + return source_entity._replace(device_entry=device_entry) + return source_entity + + async def handle_energy_sensor_creation( hass: HomeAssistant, sensor_config: dict, @@ -933,7 +955,7 @@ async def handle_energy_sensor_creation( ) -> EnergySensor | None: """Handle the creation of an energy sensor if needed.""" if CONF_DAILY_FIXED_ENERGY in sensor_config: - energy_sensor = await create_daily_fixed_energy_sensor(hass, sensor_config, source_entity) + energy_sensor = create_daily_fixed_energy_sensor(hass, sensor_config, source_entity) entities_to_add.append(energy_sensor) if source_entity: daily_fixed_power_sensor = await create_daily_fixed_energy_power_sensor(hass, sensor_config, source_entity) @@ -943,15 +965,19 @@ async def handle_energy_sensor_creation( return None -async def create_energy_sensor_if_needed( +def create_energy_sensor_if_needed( hass: HomeAssistant, sensor_config: dict, power_sensor: PowerSensor, source_entity: SourceEntity, ) -> EnergySensor | None: """Create an energy sensor if it is needed.""" - if sensor_config.get(CONF_CREATE_ENERGY_SENSOR) or sensor_config.get(CONF_FORCE_ENERGY_SENSOR_CREATION) or CONF_ENERGY_SENSOR_ID in sensor_config: - return await create_energy_sensor(hass, sensor_config, power_sensor, source_entity) + if ( + sensor_config.get(CONF_CREATE_ENERGY_SENSOR) + or sensor_config.get(CONF_FORCE_ENERGY_SENSOR_CREATION) + or CONF_ENERGY_SENSOR_ID in sensor_config + ): + return create_energy_sensor(hass, sensor_config, power_sensor, source_entity) return None @@ -976,7 +1002,7 @@ def update_registries( domain_entities.extend(entities_to_add) -async def check_entity_not_already_configured( +def check_entity_not_already_configured( sensor_config: dict, source_entity: SourceEntity, hass: HomeAssistant, diff --git a/custom_components/powercalc/sensors/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/sensors/__pycache__/__init__.cpython-314.pyc index f5d2265d9ab14b6a4a31b19a14022f41567ec4be..ae4b9c568399737ce540f9529ea48d76adba98ba 100644 GIT binary patch delta 19 ZcmbQmIE#^6n~#@^0SHdN(4WZN4*(|m1fKu^ delta 19 ZcmbQmIE#^6n~#@^0SI!IsZZqY2LK~h1NHy_ diff --git a/custom_components/powercalc/sensors/__pycache__/abstract.cpython-314.pyc b/custom_components/powercalc/sensors/__pycache__/abstract.cpython-314.pyc index 3e0b741661a11417766888135217e86acada78f5..ff1769df9eaf1ba3cb58ce8c547c82614a3244c4 100644 GIT binary patch delta 1765 zcma)6-D@0G6ra13{g}yqWj|+jyV;MVn~qVoYK$hv)MS%%Mw4wEQfs!QGLNI13LBo)d47;ibT#AVq?QYuRJU|O<+8_@~5&yL!<9Re<# zmiD51gq?z*vI<&u?^ME*l}PIE;I2@&@?-_w)qtPxs7q(3&<^$d zSF~9)dKm#Dew>@fWe6dNPz87-j2tX_&jU5fjUa~kIzGd{3?@3L+f5E9yccP1UTHLU zMFpI9>sRSHWVIdIs5iIwt~!q&3q>>N17%T>6Bg{m>{r03wIK0Q=sd|CT@G!MY2i!6 zlXvS(l$nYW4xz&+fZ=yjwUBrP!L2CwMxKj^u8SfdtZ*yxnb5h?ISh*S(h>Z_hakFv zf*(oyfs~%oZZ_U;*PX^T6j-B?d16AjNRE5vQ*F{-sXIJrulcww=G_ny;!3Sh{4c3NA zFCz6&gP1#P5%nTq8WhFQHxTf~&`SW1r*@nyd*1~9LrCpYNUNJ#I436ZVXK5dF=78} zuDU#thH7&w&3#1-TjIZ_D`nwNxOxiHkO81P?)?=~5W4(%Z7PEAh2g2y47mm{&O6#1 zd7EF?a{Lc1=@aAQ=c>L)#`vmU@?o*50z{T`y-12cX8BLLpQ!wXeufNkk{RoW8!2jp z&w<-|)oO)!$=bUtvrY7S zzZLi3vL}(jknt@j_$DG0M8txBf*^=cpX4%?A%YJ+Y$){AbG|pTvIlD(KKY&>_nh-N z=X}4PPyH|z90>(w34Yt34c%zo3o3-(8yat`Ugk?nuS=S%EUSLzR|6~{WJfuuhISok zNe#1z(74OpYLrFQ9@f+Gh;=-AS)b5&%5gQp65{MFD{7J@#o1R*sr{^9J;hF`18hJ| zv$UFF88yq=S+$FGrKPG%lSi?-&X47)j@8A-((!**st!&PTV|(wrL3g+HINv#J|G23OblaHOL2KbvK9vso) zU`?Qvjq*K`R~(jaexeSCCd^00)v^Y&hMTP>Q zNeqZ>&Dojy;YblC_=mo8WPl&~J{pXmG7PZqq|f7d7U2SbiM`L~^EAcp${AM)qzV3& zT!0Vn%Zq1c&=0x%SEeF53BYWt{1g8(xp~wfPZ~s6060zsf|cTjy>Yo&+cBsLa*-zj znDkE1EWZ_qwXp+ssebehgJYvv7roHmFmBR0)F!UchSA*GzNtTH7W~d*3?xY}B4F!I zHUXlN7zb^{bVcBwC-;xdBWU;bC<*|31j* zmX4$-aX&)%QI0=!@(&`Dz|k9ffpB+piVX7e(Og>uUB<}A99+fcm=@p0>I|w^O;OW?5>rr8~o?! zfa^7|T<4_cebK1O*_ZH}lZdv_=?VgJdWS5%4CjJO*HJZxAXtf=#Gz4SB`Oho=&LB= zZK4|hPgKXh>&?5WpuYuWjq`lp29$QYZ{ecY)8hdOo?`F**S76(XB=k0r{mn6!_-gl z!}xe!#1o;$uoU}>+C8%AAg4W^Pn-+m6H{qNR4hNvlwt>M1~|>*_tEms w?zFacw~Y&wf&rg1^HYF_PD02}QtnqN^qVw)J;6VYi>DdSYfcB%!|HFOvtE7n-xWr5Peug z$*I(e>Y{Q+hVqCMML|XM0l{cFlP(G(^dfkzNVs&QYL9ths7HW=#Qf5Jvuz(I1zDu`Nes`R#QO&3|t z7n3gV^IhN+=^48SFh?&su5d7muJW^sTb(dd(9s_2=-kuR9cxSM+TXLY`(Vsz!pG%T zie_}}i9@jtITP>P+kK$Z$;W$GulNOQt5~nf&QXPv70;JW+Z?|{WqIytMOv^+Y9eYf zf;A;r2U|16fmCr|N~r&_7Ee@0-K*6w04u$U!8}H{i&px{)22FxFZ_}G)gu2=$+O_4 z05Xe=5uC@%{<5Zt5+?G-?8+vX&uTIuW&J^b#Wd->p@1Q5uHB)^*0I`nywY0z-VD&-JSFKO|d zYa`U)H`g<8&=I#9z>briKB%NCyxReW@rEx16*%L20=0CgE(9=&Y5!WNMfo^i_fJgW(?1AsJurUD^cE7)((8594 QpyuY0v8}&}%-Qq*2b-$x5&!@I delta 767 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 LxBi)f8&UMX#4Ghv delta 13326 zcmb7q3tUv!wf8ymVg?3g5QgC~zzpv}-fsjEQ1pzV;(+)<#}Nh?g@K$oXu$`*V$55! z+r+dcA?6{D|E*NXr;(F0Nf@r*xF9gXk_&s9c}C8Jc5VPF}5DASLmg5tnDt27kJJqc3K1!Qx z+qoUW4ob({zQ^?o{gh6yB@S>qg`M!i+7oTNxZT2TZjZ2s8x(-Cjp{m#X_lGhYQa$PYXSBVCqUvJQf zp-sHAbz76GySK|JxJG`Uj8d^i@_a}JYbC!6IjZc2wgj?GyD?_~s3azl*~_$G2lUs> zmcz3xl3`3t%Oo2NvxY5_k&li>k*~G+%Ge;LUPW?3>)A@OGj#suEl`1PMQ|as19)** zY4GE_fDj{Ec^B~F=$J@j6Jmd&ib}_&wz{KZajM{W;_gGNY$A;|TwyTC( zzpi#vT}upMi5W^pzmT0!D7C6Dz0pu%e{2PNoGn#6VW?nVV#^gH$_n;nwt^*9ua=V| zA-Y`N38i?-n-M9>U>Z`CODj^yD&K+7NiKv%4{in0%JSRb;YFQEqU`f1F$(u!Mh(DT z<~GyJvU4UVUJEvk%klL_MEtlCiE$MZX&w)vq?(CH9oJAYm{CPfgdoX#pp{mXQsAd& zn31(QOAxz-3`DJ0Q6~2hF)G!7oQt87?0Wg0c6XPHZzqw)@-UjC>g#O-;qv)pgK;HW zN=_P0tc|>B%uaDYMLrBchp-P}6G8>T284AaCOXZY0i-B*ds_J=lybJU@pYKJ2wx*x zdpzy#jy|3$p2OSI$G5upm6)YkUbA9#O}!Z8s9(8cNu6C(wz;c)o|X>ZR%Vrt0wbVzEOAl;ML&-Ho->UIAEeb@cQ5F%2h!KTN`6 zauZO=_`3o?XX)oMNvX!eR+0@d8|M8{7dO=HPh00pTjw{gd(XUXbdz(`z1{B~@VN&* zXP8~=V%a#uaI&ScTMV<6T_%G>2Rz;%xe=4hW{ibp_!~A3XZthj-_5KaYwh#5?(((n z8dLB7Sgm_F^xn{M857FMu92L$TvfP66a0A;X^pGbEtfGdtJ#}jjAcDLv5Ot~_c#N) z$%(A~Yt`(8B40%`;DTw8=G?r?yrReB;%8IR8zeuzob`-si$5eY-UrQCb^KoJ(1x%O zK#U@L3Zh7Yxrkj&8qF&PS7Bm=+v{lW>+0%v1m@e-#vjKzPb1(wScC4EF)_T&?QQMZ z?&A9$-A<3Q116Tv-wCYpQvh!e02l%@{G`=%KV~7^fPXJYnHe!#USikXAr#t7GvxC9 z2x}1NqOc-`;t>_!N*)ZEo4gR@(UWXs=dHzhbG)uDSF7Og_HA(l78KePIz~PMD63K$ zC}o27?;__@Qgl?&UcjOV47rsu5;B8tayqqW9S15vZp z+v9P-l6Ufl;XNQry3A>}_h3=X@+twSa!9K27-k?Ok=N$_hV_sKvlcb{qan0mL8=2f zyUW={mr0}KR%RPWC9!0Kl*y{>NHQ3&44TJMCpglbUC1hm0XRwQ=l)jKbPDQoXOLf=ZK41@(nghKemcJ+rIlYVt&0ItcFcN#n@-c>;C{D<|3JFb%VabhQ&Lrow3J9B-9Avu60J3n`x&JE@fe^y-3JvW-3@AJ2{xKmhb{F>Fy+dD>Jcgk?8URja2)l z9p8S)=hq<|Kv;o*E02E#;ckGh!20n4&qsp-7iL)z_95IuJ}r-4ipGQ*N6Esq;p@E! z)WWqR)q#NO&W|AcGvy%-^HSKmNZ-66Fm_QDHx<$KSo~oCQPtMd?R0y+^Q~T50dPf@{$CsZ*VvTfp@@qb_?KTF_!7! z`+1xPJ`*7e!G@57kWG%)*g|TN+>5Z7jMqfPqR_-SjzBQvaC^W(cpxInBj(!t`Bb`8 zuvDg0oG3{tq@vQ31czxDlFn zZT$gY7}XkGDTc~&W!VJ6kb*vV53$s}$-YDWv(B8uLP1d%2ofh_j}t7fzy{!-k@gV$ z5RhUV$?uQQ1PTw6ro}&B;KU3y?~?Wyl4YbGgtTdZVpBggP%zD*xAclfmKv3sMrN~;tQt0vvZYtd z%?drUTG=PdWBO!^;~Az`*%BJ4*{o~{BYWDTN!NxDlX9DESt7$|m;@-%4pXV;UvIs zs#Wk9CbDl?v@u!An2C){LkbE1VXO+eU>efM#DNIdAv+gDEXTD-f18*zxe>(FvGs4^feuc98!$j&L}Cpi|Ff(SJd(ZMnl!s=lPBtH*mLB1eJben>64Kp zX8D9_FLcj8O!O->r+_Ci!sSKx+GQ!_n14@xMBTPBF3 zfD*&p5O}(sU5-9-dUc(OvimOidi9RHYYVuySB@>%aBR_2w#RLLQPIud1bCl)WPPub(=$& zJU8-+DrYMmG>H-5?Htk>1Nf}2t}RfE96C0y3j%v5awl^*26VW?7u?;hfUo}c!oNLJ z_-yFB2?k&&aU36K7tuHBFd~V<*^-=(BBi=Y4eq3Xv{Jk+aY=^i3%1To_zU)c!vE`D z=7v7fuP^lJ3r{V2PhU7SiVLnw1Gg4@icrPgQ1yr&_Vi>CRibK znT%&L@fN?S)MqNaYAPR9$9y!ml8kIz%&N&(8#6++>BEW_H77LZ7yM2ukl1yW!H+}c z94YcgWcng9N3&}DA+_&@)Lzr-{n})oHrcN&^=V7bD*R=ud}XV~wDue36u-I9XD;-c zOMT|ju`>H;)9S0{=23P0wGi`A{#Z!L$DxMXvVi{Fk~3jZdk(Z(PWA0>@RONvy8ck{qkloljmW-^e%(pADqLf*hkqV zC5p$4K)t{&DObE$69ME!S@e=h#l=JwVrnI%T(U%Stm0CY3h=UGVId^FQO+V(26HNs zKQu4ciL-+XV}M*=7nd|!K=3t79%wZ|7(F)O6B>8@)LH0E}aa0gcXz;{ zRUjsZoj%=qB*E|ow2kPIzMDbAKrV^YcbBLO-rLhF+4{dh zwsf9FeFpUp7%q!?v@#AF;-WQxgWcm;^a;$1C&Sz0$-%n{tC2zeQ_O(CZgR4h17%f6 z>%IZ=v5=^wRwHmAKPiq_5Jxo8L5ZZ10{%X>&WWw8f%0F4O&(W$_i@(|R=`mD^iN_& zs)Tn~>uqe5>KC0Q3-UxQ7Tqt=A%d7z6FIWIn5`pM zwhu!$a6B;28DLx5{Czv8gLAZ=e6nMbSCaO<4>N9ILqP~l&~HHPQ6S$4?5jvte-^ur ztn06w$pIY-okGgNtfv#m!DgZ#z|)Dcf$>roc1-o)3#6tcgBlTFh6;r0^yH4HkvMi{ zuyE$Evsz+C3V`llW<88y*1bmuv^y276)O`a7=Yp8aeSQGMBi{p{v?v@eNrj=g^FB% zFoBF73RX0+y)trrtC4*FG@LwIgEeJfeA-~TO{DsMBU$eWCTANXiGGhVXd%YU%?vU3 zMLu#&6O>0^S#rL|K%RfeKn4#e$>@HS75y~*wV)r9DAW@An+nHe%MXy6`=h?m0Z0)# zkXl@K$Uw+x+OeV#rMCYIq?^53WRuz>;o zb7xcyS1Q(2H{?NzfxNXLLY@Yv!TQriSwkVM^Um&YvgrPZ8Ny42Q>N*j!}FER${E5@ zOzo#M$jq9itf_q@?kb`45^~pl7M*g21TZc9hv`bpErZ;+@V#`5Q*Cv8k33x!tLjzJ z0>CLd#jhIp#UahZdYarx*+_k;{&yeqRco5R$9bOoNaE&ZYCId zeGUMJ7Sw_!krRWd7588=nlCXLqF2~F^mt(VFxiN74?B4?L&H1CNkRm~h)HcNicT_i2kT6)#Aj5zkD z3_@N^{Nd1RAx3{lye}kvC{k*5EF}M|;9s!bw_yF~hK{QXI{oFH@0EAHuZuaB;*ZVu z#paI{F7)dby{lUU+bMrkwl6B%A64Ots<;|8->;uPs-J%?Db1f$Z;DC_-k8ywJl?tz2H2ZY>jL} zdcX`Xgtu$Plu>;Od3$ei=xr6Fjk%@`|D87Ja2;vwPgcT^w0iRL1MPz!>!ObI-M8tH6yd8FcvoSF)F`IVJjZdCj#GaTv47{FggWdpvY2tw4>Fz|?fI~4QZPRs-R zO*o4P4FVbg@ft#M+;@1Y5!Bh)g5oF1(1YN(M=n2jj@8qXI2z;r9qXPUM;}g1l~w_& za)`&ZBXGed9q|Xk!PTT^OAPt+;RN>*$DFMr=n!g?(d(vH{Xe^Q42tA!ECGPhKw8^iUV$Ruf-7G z5Fa-kNNBM~_zzrTxX7lbQ!Up~+EI|YmjSB{M^d^ZpDxL-%kt^6M%7smYbUl=&fp>) z`I(K2KgdY{On1?G>D3j^f>PD%8U-?tqTp4$2N z&a1k)qw2Z91Z=US3*&>>(swkApUB8VKar;BlY(iLFT$q&8{q+jxc~uMhrLYMI`a0v zHOQcigrC+(yPJhLDPjnG)z%O9)!Xo@{5x_qAtV3sQ{+hX%&%2cDIO>9J(CC*UVnLp zlR<9vvpdqi)!D^gWaPtVQPdVte&hOu3XCN3Ez!s5# zXeC@L0Gw(a$H&<=`UZ+iZ}Y$v2c3>l=(j^@_}*YIKIXtN8Ob|isCyCKC}*M`g*f9= zATQsCjNm!_jFWw8l`WV?4YwSrr^tP0GUi~r^y`Me*~d$AER#SF&LE)+J z8CH4Hs4nT0!k@R$m$%TL*W}A<@@F=^m)SJBw#C18n{Vwl|Jpt9H@bGu`y&Itn9Bww zGjrh^vS4!Z{|pXp!k)jzKItNY_>kfMjPO^4n+P&2LJi^{@s;vQ4;JW-fjSS|i%923 zG*F=@HFS+q$3x8*^*%Hn!TpHjx&F@N1NjwJ-j9GQ*2*rn=J5X)U*AXg0O3P~>j=R_ zJe$aB$yaAn*hrH2au)l0Qvb4<)sya*O@m5odWe=ostf_o7Wr}nItS=+CHGi`R3ifJ zRQS~hbgsIPqLy3@F&v!S3;ojB+9R0pD8iEnKSp>Kfv(px_^m5{19}%jC(pfGU0q!c z2WU$zl%jTf(o+h^BSI%;+-8X5m(4nQOFJ&z(tc4dBftJ-o(fJ(zVNU_|4OqCRPq+; zrSwsUfFE^`W3OyfU4&-bspN03JRqIBp%f)s_Bv7@BV0z-(aDR=nB1YfwWmS#? zJ~e>*4o3!Q{PzNCKJNH1fHMBCEXZYOw-^@itq9-=7qCp3IDv2sVljBY%GJH*jJg8F z{+GeHOb)%45QFjOtuR`fI?jg&?;RXfCs2o3p3lF9HD}{s9<;%~jU}9Up}!ZN(p|2G{NX}sNHEq3LC_Mz#iR^8+7TlwB%PUi#@eA4@J=jMK+q z@goTI63k3Kx)h~_?@4=|LTe{{NE%PVE+w%EB>z&K>Iya(hb2;7+u;(6hwl|8?hGr zlb3HIzj?h>wHh-@DF>gv9u_HG?FBZV&6rsg$Rzv~gK7=7ww642r2vkf&tEBI*8^sf zgf}#*4bx4%a79DxZ$u2XV3OLo1^9>XfZvFfHz7DMLjxYK9y9`EOcnMx=I{u2A>5Df z0>U|j#}IlEoU&R~7#`=qCIz-bH}pa+YO3V3PiW z2_qqI=4Pl^*%u5#7{iubXXbyv)PBH}US|qMnS$H0AXavp0r)~sI^KMUeoOxaFC$;S gSzsPev0=wdLpwibkotTe)W{|t+4dI(9+Zv$5A?3qsT_a0^KzjDEz2EvZLa z0zoVZx*-KUSiShrLoyF$4?XllL4m8TxRH>(MD#Svie?dYXEo@+J?Fb{?)QE7+&kX) zw%4^=bUA>;`)_*&FV46On}Y_h6>|t6gQr*xf3^KUr}1a@8k1s^GHwgWc*q|1wU7oY z!qCwVz(WX9Yy^xsyBE7{i~KUf zmGoX9z=n7O9mK8f1Ril8N3A&PjHj#YUt?kszAy$J;hKvLe}H)&GWWS?mx)nq}EO}m9SaEc~xAqOa$>9mjl z9>sK7NCYWpx-BGu%WrxtphJ01UEu-?Tp$;0%_UAQai@wjRn2BPb4&%X7mnr_ea)$58EvcR0kIbi z3xx7jqjJQ_zP7U@<1dkM! z5>_fiRX~Cc67&NUgj7X{56XTN5PZY|l|LL3WTPS!^#dtcjIk|iO=a2F}= zm6p;Pa_bH8J5Ff}{$*P*eL|fh2WeBi*+6y9f2y06OCr5Xi_>wfo9w60YnkBxQk0T7 zq0z&dpZ=)H^on*;MfI;?zk@l*a`Z-T9~q~U-amOC7F?&3zKN9C*HF9-B~n+C%Sy7L z1h&J55=6eCxnt56(3&snNZv$$_nBOZ#bN!xzUDr6L~3?Qrqn9$VKztN#R0OIB*k8C zF_lAJ)q@N(q}b0@vm+t~OZ^1Vn$#a5+s>mq&TIaAJJAsNx92>}DkaGa2aB}Vn9%-{ zKr2Q%Il6OJN)n!S#IVnb)9lnr*b{Bv4*G?WL*G9epYro4be&!d#LZ}a;f1eH)sCN7 ziB8pxsmj<|AXFK@swd70KbU>=T3|QVwk`*=`@c7)nrfEaL9~iN-;Ue)ZMW8ap$mj-U#nlkdBT6*R+Wx^rnJfe zt58&mDxs755H&l#dNEHTO+aW!A=`=Lnm=gsINAbu{kDrSH^{0jGA4l$wq?c?@ENv; zF%`T~TVu=%o{;ThOb5fX{frqPM4AC)Pvx-o!y>}ptrN)^nLViw9PKA%O?STU*jBE@ zIvo6Qc8{#x@fC=%xiCu@YbR_9a6{MpsfDBU*x Decimal: if self._last_delta_calculate is None: @@ -267,7 +272,11 @@ class DailyEnergySensor(RestoreEntity, SensorEntity, EnergySensor): ) return Decimal(0) - wh_per_day = value * (self._on_time.total_seconds() / 3600) if self._user_unit_of_measurement == UnitOfPower.WATT else value * 1000 + wh_per_day = ( + value * (self._on_time.total_seconds() / 3600) + if self._user_unit_of_measurement == UnitOfPower.WATT + else value * 1000 + ) # Convert Wh to the native measurement unit energy_per_day = wh_per_day diff --git a/custom_components/powercalc/sensors/energy.py b/custom_components/powercalc/sensors/energy.py index 7ab5b68..88e637e 100644 --- a/custom_components/powercalc/sensors/energy.py +++ b/custom_components/powercalc/sensors/energy.py @@ -60,7 +60,7 @@ ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}" _LOGGER = logging.getLogger(__name__) -async def create_energy_sensor( +def create_energy_sensor( hass: HomeAssistant, sensor_config: ConfigType, power_sensor: PowerSensor, @@ -69,20 +69,20 @@ async def create_energy_sensor( """Create the energy sensor entity.""" # Check for existing energy sensor - energy_sensor = await _get_existing_energy_sensor(hass, sensor_config) + energy_sensor = _get_existing_energy_sensor(hass, sensor_config) if energy_sensor: return energy_sensor # Check if we should find or create a related energy sensor - energy_sensor = await _get_related_energy_sensor(hass, sensor_config, power_sensor) + energy_sensor = _get_related_energy_sensor(hass, sensor_config, power_sensor) if energy_sensor: return energy_sensor # Create a new virtual energy sensor based on the virtual power sensor - return await _create_virtual_energy_sensor(hass, sensor_config, power_sensor, source_entity) + return _create_virtual_energy_sensor(hass, sensor_config, power_sensor, source_entity) -async def _get_existing_energy_sensor( +def _get_existing_energy_sensor( hass: HomeAssistant, sensor_config: ConfigType, ) -> EnergySensor | None: @@ -95,7 +95,8 @@ async def _get_existing_energy_sensor( entity_entry = ent_reg.async_get(energy_sensor_id) if entity_entry is None: raise SensorConfigurationError( - f"No energy sensor with id {energy_sensor_id} found in your HA instance. Double check `energy_sensor_id` setting", + f"No energy sensor with id {energy_sensor_id} found in your HA instance. " + "Double check `energy_sensor_id` setting", ) return RealEnergySensor( entity_entry.entity_id, @@ -104,7 +105,7 @@ async def _get_existing_energy_sensor( ) -async def _get_related_energy_sensor( +def _get_related_energy_sensor( hass: HomeAssistant, sensor_config: ConfigType, power_sensor: PowerSensor, @@ -137,7 +138,7 @@ async def _get_related_energy_sensor( return None -async def _create_virtual_energy_sensor( +def _create_virtual_energy_sensor( hass: HomeAssistant, sensor_config: ConfigType, power_sensor: PowerSensor, @@ -188,10 +189,15 @@ def get_unit_prefix( ) -> str | None: unit_prefix = sensor_config.get(CONF_ENERGY_SENSOR_UNIT_PREFIX) - power_unit = UnitOfPower(power_sensor.unit_of_measurement) # type: ignore + try: + power_unit: UnitOfPower | str | None = ( + UnitOfPower(power_sensor.unit_of_measurement) if power_sensor.unit_of_measurement else None + ) + except ValueError: + power_unit = None power_state = hass.states.get(power_sensor.entity_id) - if power_unit is None and power_state: # type: ignore - power_unit = power_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # type: ignore # pragma: no cover + if power_unit is None and power_state: + power_unit = power_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) # pragma: no cover # When the power sensor is in kW, we don't want to add an extra k prefix. # As this would result in an energy sensor having kkWh unit, which is obviously invalid @@ -269,14 +275,16 @@ class VirtualEnergySensor(IntegrationSensor, EnergySensor): "integration_method": integration_method, "unique_id": unique_id, "device_info": device_info, - "max_sub_interval": timedelta(seconds=sensor_config.get(CONF_ENERGY_UPDATE_INTERVAL, DEFAULT_ENERGY_UPDATE_INTERVAL)), + "max_sub_interval": timedelta( + seconds=sensor_config.get(CONF_ENERGY_UPDATE_INTERVAL, DEFAULT_ENERGY_UPDATE_INTERVAL), + ), } signature = inspect.signature(IntegrationSensor.__init__) params = {key: val for key, val in params.items() if key in signature.parameters} - super().__init__(**params) # type: ignore[arg-type] + super().__init__(**params) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] self._powercalc_source_entity = powercalc_source_entity self._powercalc_source_domain = powercalc_source_domain diff --git a/custom_components/powercalc/sensors/group/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/__init__.cpython-314.pyc index ad1c6226ea6ecda98548bed076d8710c451a1723..8e9d59603cd52c875964d123827da8e746eca951 100644 GIT binary patch delta 19 ZcmZ3$xPXycn~#@^0SHdN(4WXX6#ysL1hD`B delta 19 ZcmZ3$xPXycn~#@^0SI!IsZZpd3IHTD1PA~C diff --git a/custom_components/powercalc/sensors/group/__pycache__/config_entry_utils.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/config_entry_utils.cpython-314.pyc index 1429a7cd7e608711d7d2960d9170a89e4dd5829e..93ddfc713505f9ad2fa8924ae9b2fee849ab1455 100644 GIT binary patch delta 647 zcmdn&dE0|mn~#@^0SHdN(9d$($eYbPd4Uuwvjii<<`!m2MoCSks@U+1)I5cfj8uiv z;?yFAqSV~{vee?qJ6U9fbrebqQW8s26%uoD6w-_GOACr8|7H2d+{(Z(iLFhhNCs$d zG9!=xVGv*gVrC%zY%lWdASioXQ0bze z(se<#i-KxPsxAu}HF(|U7QM;Ax7nG;fm=%hXh;LYcLo+lwvQlUN$v*_btD>0T?vi6 z5|w&2Gz};>xkE~J^Lg2qMx;c^`f-VN07b`3{0$SKR+=5nUkkW z9@6gu8eR-@6+6gPKw^pQ2M~3H8%$jZ2)PoPa5W$iD7U#oN|K2Utf^)4av9mlK?*u5 zypq>>6fg29E=XOTwJ>XW-om^qrhb>z{I9EpUI3!t(2G1_lNTsRPxg|1$~bkif!u#a z#wnW(6zmu^=7X%4hXx8W$a)~L1{f$n>Iln`=&KHaV4=;k6vY{R=7Cf@gH?Y328Y6R zX}ycmde^0GE=t?%3A-Zg0u36M399G$Px4=huDIv{^}P$a?>B!|vSeheo~*0lX9|pg puY5oce-L3{;`+wK#>n=W8OZtoVto(-vc50@-2)c;1Y&^`2mrd;!+-z) delta 723 zcmYLGO-vI(6rR~`OJMoQ%AYmV*%k$i5P?JzFp^3ju7ngYvIH=NhK?>+e$pR_Kn>Bv za1)()(&&YF@}SuZ#+&KIlO5S+MGq47poxbTG{^;=S%|(&zVA)ueeb<|^HDV}6rC(C zDgZ1ZPirSfUld()6y63v+*Tj}K7foRhZ|O%c1*w%!u;%@j;EElj+I0VN0lg^nz@ci zF5gqD%wc63hE60cAJyNTD+bojEsm*R0ztk2ueIFdD&3L6N_^X8jcC>_=_nrnWu;T>z#mml(^7folEOcdo=ydIF0g?Qu`UdYC8P6XG?-?1? z4(lKBm8%a{hPHzopD_nHr^M7q#^0Z_FUA>fCG2>}6|J^n6IENNnWAP3`6=?JDomu> zMyf0LZSR|2b99mR9C)Sv_sV$eYh}&J`|5ziKkZim^Cx?VG#+!8@uAxeAQWr~JdIYd GL4N^sK<0n| diff --git a/custom_components/powercalc/sensors/group/__pycache__/custom.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/custom.cpython-314.pyc index e91b5fa4b3d3b264ff811a4e55838dec057b4f2b..a413c92855c30063ed111c530c009dfac3eb2e61 100644 GIT binary patch delta 16747 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 delta 15551 zcmb7L30PEDvc9)Bn(l^fpxJDeZuU)z;d- zI(6z))v0@Z-CercwYt>qvWIiaIdec+b7_mKh0%d!j?z|FYv~%-no{B-rB0W#)a7!Owz=Ba z^PsY|rR!Yl7#&=;zO>!d≥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 diff --git a/custom_components/powercalc/sensors/group/__pycache__/factory.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/factory.cpython-314.pyc index 8e5685b092961887a3556080645b8bc2b229b0aa..b1876c3b7abdcb879936d02cbab35e8ba5b7f3a3 100644 GIT binary patch delta 354 zcmX>p(<#lX&Bx2d00gIB=x3E}{)&6zRW(}Im%BIdJIa!y@XmUPVDI?S5r)+v` zK1y)VBip%5_w%z?V_mK6?U~d{4&@1tuOIg?{K-yA8>&qVDfuT zQzrqSpy?%k(;I@KH>A{Wh{{fnn-I4^=nFdw$QTx&F+5OX)GvyvUtw3j!!HXl2Bbq+ z;S1~J0xlo-kJ>CEd>_>q1l$;Z7MlZ|*yPOM#=&@)Sq4PPGrF;`99ED6vF#lNfz(k! bc@Sy9=*Gr!)KCP-KE}rA#<4k``xO%aa6n6(8<=U*1SySK%F1}QaibsH3-AFE36uf zij)7c8ez!jv6V40PF~8UC*jKIqsGqQ^A(790`UYOjs;@5$v4^TMEo?Fi}-;y++t15 zD=Dfh5&*I{YqLLPWGtU7$muOAs(4-0=8~w*4IY6TJd&STnZ>xiGfd9mloS?Kx-RN? zN!0Ox%4N~8>-=FCIKn1Ri$Lq76>1o;>Q+!%ir*8v^f(M^TrpuG{0eaMi}O`YYCkphr?SXc%` z>NC1&vK%&$1G10sbAw1JMmKGiBhn^7_ECODH(i#a0wDEA4dsF4F#$$5eHE5tf+8S} YIwOeG1e4leQWs1D4K>_6pX(J90L^HIu>b%7 diff --git a/custom_components/powercalc/sensors/group/__pycache__/standby.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/standby.cpython-314.pyc index c971e838ed3f9eb08112a8848a7e441afd9e19d4..0fd68e9ad0ce68cadf3102ed6cdae17d74c44e5e 100644 GIT binary patch delta 215 zcmZ3eenE{_n~#@^0SHdN(9e?G$orC+gPDPW@pB9#!(=9wZHxhvcd{f+)@GHN9L}o2 zq{%$Fm(^arf=?LpTH^vvZ$GXf%h{LBLjmHgFHjS>oylg;@pm~9>HHy82ivj6~D Cs5GDe delta 337 zcmcbhwosi{n~#@^0SI!Isb}eLFXfkBBue)2!&ZHx(%x3VPJxibVY zXE8G{R4_I$`>=8`6tKoff;56a7(*CK7;_k_2@8aep_XlOB8&KBA69-ob_O3^AeJ;` z&ScbNnHLJOVd(ByaFY z0Ljmstnysn8CWH`I(R?uFmMX=^L6q~sJ+6WdV^c2!SACWgQOSZM<$^B&teszkxfnl z&Mb_FgczL}SPlt`g4i02&P*(aG$nxS!%U1oG8y7(2+6$JfW3*8n*+!Lh6L~DcMOyF t@~AVqPkzc1!eqj@*`GIokuh%a20mTp%OYlzpYd5R8#|hAR^-=b0RS8oO~L>G diff --git a/custom_components/powercalc/sensors/group/__pycache__/subtract.cpython-314.pyc b/custom_components/powercalc/sensors/group/__pycache__/subtract.cpython-314.pyc index b72b64d9b45203530e05d3598d52489aaff490e3..61a953f8ebc5fc9922cd5c5fcb90f249ed32773a 100644 GIT binary patch delta 166 zcmcbu_gZB)!)5`N4~&d1lNs4$ChM}Pin4?; z#;Aalf`A$jhj9d`PiABlo?OKyE?~k47H5zL3M(^cvQFO1ma|!lJ%W*0iIHJ)JI57X z37s1}0ylVsZtzHaW}lqPsV2jtctb$shJfr10kO~QEWCVQnAteFKJYQ{2sijmF6VaJ JtjT4>3jm)pB?JHf delta 273 zcmaE_ds~lJn~#@^0SI!Isb@KFc`>$Z82x_i}Ow#}y$tqZ>Q|H+Y0@@JM`SXOZXn&M?`O zQ!P|T{f2nRa!SqTPh~?L!G*2w2iuUBMB1OYo=vVEA@~40ZmbsY1($qeL&Kx z>Gq`eyXT&J?tS;3`@84d_tDIsW~_Bqi&22*4<9!@&>LK|7L#Wdt9#RNlWZy%?h}&w zggI`JEpe-CjoV}!*X1Sb@q9U-%i2Ui+%cyq7vhDolWPp1DUyrgF4@KF#zb+vL@wd7 zDdCQLWKXTf@L2lq`TcR=EBsX!{o@kD@$Sv`(9Om`> zL~A@EN4Q*&h{j`bjLVKhTYQ(it6VrD4pLmyXCJ?CWOsz9I=RCs_=RMlOGrA8Xd|uK zEp=zooh$;S%OyZ^ z@n;`gGu3{o6AUFEw(fale0h4U^=@80A+6;VujUp1xhS}SUDgFrCv9j2YwY?b8mO~& z;~}z_EgIiT;zHEJx|^#wz!Y0XWhVnEOf zJ*5Bd91n9W)eHTn;XTX(j@@otzoMPU$W#v!EkZytDQ?!}al7dd$`Z{?XCKNa;<0RY zirP`-VLu6Z^!#MdCicFg)*i&JoK7w@@UnD+*GijF%ej_ElPp~LU5OutDn^`D?4!cl zB<>CkYjFBvS338`AoVvO_^h;OxZ6+-15a}#0*>2r$jbfVF@ z5;Qv3N%~ykxjkJt)VnrXUVXM z_X9|gVGL{h*^Y_Z44VL}Qeha+o7%or25!=&$Ts0XYiD~){5s8D!e9X#vN=d0M;F_$ zIM}+)ph;=iFG`|*j7a?1?mDX-_HOHV=PtX$qurjy7)P!f;6}{N zmOe@zWmik!D%Y(fLk{Xav^;G~R!p@ud`II|)F4 zin;W%!@E+}_#1J}5xwAuUUs?J+)49gN70I_`l6#|#azSk%VzD}q}xQ6Mbd2$mvy>s z6I(3XOQLM0EIf_z?aRH221wZ_;LVi*AZ3em;8}nxw#d2;N;s%S>xLdAmAYH07pw&V zQ#GeZ`xVpep61BZH&kkXNobRGdeLBE0U2!N7DGZVM)`1XCs+5%Oz#op(Pr;X29Pg|c~xLDkF z!g9%!fAXGHllS?C3l$xgY^7_y=y_js)fQ!s2Y$S}xx1RY87AF+@y%A0&jv_$Ks;O7 zrG?7xYr=rbZqgkTmpyuvp&2j;h>@}RMr8cb!;4c!n!UizRhC0G|G4rzIOerrh4%n1 zB#*BW+!`Y4amCPwiTir6mb}0!s}^~_R!q6fOfE}D@)hyL?0hGO`2nvo*6r;d#(|N@G+3->OhCsuWB3W{(q0W z&ZN2?5?<=ByOX$h_*}%Xo&X=yIr?BB^gfCh&-j28Feq(XZqWO;y zZmduxI4p5{g-4-XY2zz|n+SQGEj0X5y<_gIP%-l@8c|81dYG@&hhzV*y+02|xf*7# zcC%L-3t-9bHGV=OeDC#e&NFmko-Z5bkJlo_lFQD~(acr|xG{BL#wX1clYHAS396~2 z6eWJ0@}a~RdJjqk2oUigvm8kD4}50pe~N8IGELLgR;)D-1&8T7TM=g>csl`Ii!JA&6~#Q+!c z{_MmAe1h)@KP>!g_F^yYjL-U7$?KfXj?GPF_R`;h34u5FB*40s5b_tH;v=E>1HsC! V#AXZOFY}r^qantt0wVYCe*ssZc6R^( delta 4159 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 L@;(v}x!e8+$RWD5 diff --git a/custom_components/powercalc/sensors/group/config_entry_utils.py b/custom_components/powercalc/sensors/group/config_entry_utils.py index 0e0dad0..4eeaa53 100644 --- a/custom_components/powercalc/sensors/group/config_entry_utils.py +++ b/custom_components/powercalc/sensors/group/config_entry_utils.py @@ -20,11 +20,11 @@ from custom_components.powercalc.const import ( _LOGGER = logging.getLogger(__name__) -async def remove_power_sensor_from_associated_groups( +def remove_power_sensor_from_associated_groups( hass: HomeAssistant, config_entry: ConfigEntry, ) -> list[ConfigEntry]: - """When the user remove a virtual power config entry we need to update all the groups which this sensor belongs to.""" + """When the user removes a virtual power config entry, update all groups this sensor belongs to.""" group_entries = get_groups_having_member(hass, config_entry) for group_entry in group_entries: @@ -39,18 +39,18 @@ async def remove_power_sensor_from_associated_groups( return group_entries -async def add_to_associated_groups(hass: HomeAssistant, config_entry: ConfigEntry) -> ConfigEntry | None: # type: ignore +async def add_to_associated_groups(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """ When the user has set a group on a virtual power config entry, we need to add this config entry to the group members sensors and update the group. """ sensor_type = config_entry.data.get(CONF_SENSOR_TYPE) if sensor_type not in [SensorType.VIRTUAL_POWER, SensorType.DAILY_ENERGY]: - return None + return raw_groups = config_entry.data.get(CONF_GROUP) if not raw_groups: - return None + return group_ids = raw_groups if isinstance(raw_groups, list) else [raw_groups] for group_entry_id in group_ids: @@ -130,7 +130,9 @@ async def add_to_associated_group( def get_entries_having_subgroup(hass: HomeAssistant, subgroup_entry: ConfigEntry) -> list[ConfigEntry]: """Get all virtual power entries which have the subgroup in their subgroups list.""" - return [entry for entry in get_group_entries(hass) if subgroup_entry.entry_id in (entry.data.get(CONF_SUB_GROUPS) or [])] + return [ + entry for entry in get_group_entries(hass) if subgroup_entry.entry_id in (entry.data.get(CONF_SUB_GROUPS) or []) + ] def get_groups_having_member(hass: HomeAssistant, member_entry: ConfigEntry) -> list[ConfigEntry]: @@ -138,7 +140,8 @@ def get_groups_having_member(hass: HomeAssistant, member_entry: ConfigEntry) -> return [ entry for entry in hass.config_entries.async_entries(DOMAIN) - if entry.data.get(CONF_SENSOR_TYPE) == SensorType.GROUP and member_entry.entry_id in (entry.data.get(CONF_GROUP_MEMBER_SENSORS) or []) + if entry.data.get(CONF_SENSOR_TYPE) == SensorType.GROUP + and member_entry.entry_id in (entry.data.get(CONF_GROUP_MEMBER_SENSORS) or []) ] @@ -154,4 +157,6 @@ def get_group_entries(hass: HomeAssistant, group_type: GroupType | None = None) @callback def get_entries_excluding_global_config(hass: HomeAssistant) -> list[ConfigEntry]: - return [entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.unique_id != ENTRY_GLOBAL_CONFIG_UNIQUE_ID] + return [ + entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.unique_id != ENTRY_GLOBAL_CONFIG_UNIQUE_ID + ] diff --git a/custom_components/powercalc/sensors/group/custom.py b/custom_components/powercalc/sensors/group/custom.py index c577db0..8b3dda2 100644 --- a/custom_components/powercalc/sensors/group/custom.py +++ b/custom_components/powercalc/sensors/group/custom.py @@ -59,6 +59,8 @@ from custom_components.powercalc.analytics.analytics import collect_analytics from custom_components.powercalc.const import ( ATTR_ENTITIES, ATTR_IS_GROUP, + ATTR_MEMBERS, + ATTR_STATE, CONF_ALL, CONF_AREA, CONF_CREATE_ENERGY_SENSOR, @@ -99,7 +101,14 @@ from custom_components.powercalc.const import ( UnitPrefix, ) from custom_components.powercalc.device_binding import get_device_info -from custom_components.powercalc.group_include.filter import AreaFilter, CompositeFilter, DeviceFilter, EntityFilter, FilterOperator, FloorFilter +from custom_components.powercalc.group_include.filter import ( + AreaFilter, + CompositeFilter, + DeviceFilter, + EntityFilter, + FilterOperator, + FloorFilter, +) from custom_components.powercalc.group_include.include import find_entities from custom_components.powercalc.helpers import async_cache from custom_components.powercalc.sensors.abstract import ( @@ -133,7 +142,7 @@ UNIT_CONVERTERS: dict[str | None, type[BaseUnitConverter]] = { } -async def create_group_sensors_yaml( +def create_group_sensors_yaml( hass: HomeAssistant, sensor_config: dict[str, Any], entities: list[Entity], @@ -152,7 +161,7 @@ async def create_group_sensors_yaml( ) group_name = str(sensor_config.get(CONF_CREATE_GROUP)) - return await create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids) + return create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids) async def create_group_sensors_gui( @@ -171,10 +180,10 @@ async def create_group_sensors_gui( energy_sensor_ids = await resolve_entity_ids_recursively(hass, entry, SensorDeviceClass.ENERGY) - return await create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids) + return create_group_sensors_custom(hass, group_name, sensor_config, power_sensor_ids, energy_sensor_ids) -async def create_group_sensors_custom( +def create_group_sensors_custom( hass: HomeAssistant, group_name: str, sensor_config: dict[str, Any], @@ -216,7 +225,7 @@ async def create_group_sensors_custom( sensor_config[CONF_UTILITY_METER_NET_CONSUMPTION] = True group_sensors.extend( - await create_utility_meters( + create_utility_meters( hass, energy_sensor, sensor_config, @@ -273,64 +282,83 @@ async def resolve_entity_ids_recursively( if resolved_ids is None: resolved_ids = set() - def add_member_entry_ids() -> None: - """Add power/energy sensors from the group member entries.""" - member_entry_ids = entry.data.get(CONF_GROUP_MEMBER_SENSORS) or [] - for member_entry_id in member_entry_ids: - member_entry = hass.config_entries.async_get_entry(member_entry_id) - if member_entry is None: - continue - - key = resolve_key_based_on_device_class(member_entry) - if key and key in member_entry.data: - resolved_ids.add(str(member_entry.data.get(key))) - - def resolve_key_based_on_device_class(member_entry: ConfigEntry) -> str | None: - """Resolve the correct key for power/energy sensor based on device class.""" - if member_entry.data.get(CONF_SENSOR_TYPE) == SensorType.REAL_POWER: - return CONF_ENTITY_ID if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY - return ENTRY_DATA_POWER_ENTITY if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY - - def add_specified_sensors() -> None: - """Add additional power/energy sensors specified by the user.""" - conf_key = CONF_GROUP_POWER_ENTITIES if device_class == SensorDeviceClass.POWER else CONF_GROUP_ENERGY_ENTITIES - resolved_ids.update(entry.data.get(conf_key) or []) - - async def add_include_based_sensors() -> None: - """Add entities from the defined areas, devices and floors.""" - if all(k not in entry.data for k in (CONF_AREA, CONF_FLOOR, CONF_GROUP_MEMBER_DEVICES)): - return - - result = await find_entities( - hass, - await build_entity_include_filter(hass, entry), - bool(entry.data.get(CONF_INCLUDE_NON_POWERCALC_SENSORS)), - ) - resolved_ids.update(filter_entity_list_by_class(result.resolved, device_class)) - - async def add_subgroup_entities() -> None: - """Recursively add entities from subgroups.""" - subgroups = entry.data.get(CONF_SUB_GROUPS) - if not subgroups: - return - - for subgroup_entry_id in subgroups: - subgroup_entry = hass.config_entries.async_get_entry(subgroup_entry_id) - if subgroup_entry is None: - _LOGGER.error("Subgroup config entry not found: %s", subgroup_entry_id) - continue - - await resolve_entity_ids_recursively(hass, subgroup_entry, device_class, resolved_ids) - - # Process the main logic - add_member_entry_ids() - add_specified_sensors() - await add_include_based_sensors() - await add_subgroup_entities() + _add_member_entry_ids(hass, entry, device_class, resolved_ids) + _add_specified_sensors(entry, device_class, resolved_ids) + await _add_include_based_sensors(hass, entry, device_class, resolved_ids) + await _add_subgroup_entities(hass, entry, device_class, resolved_ids) return resolved_ids +def _add_member_entry_ids( + hass: HomeAssistant, + entry: ConfigEntry, + device_class: SensorDeviceClass, + resolved_ids: set[str], +) -> None: + """Add power/energy sensors from the group member entries.""" + member_entry_ids = entry.data.get(CONF_GROUP_MEMBER_SENSORS) or [] + for member_entry_id in member_entry_ids: + member_entry = hass.config_entries.async_get_entry(member_entry_id) + if member_entry is None: + continue + + key = _resolve_key_based_on_device_class(member_entry, device_class) + if key and key in member_entry.data: + resolved_ids.add(str(member_entry.data.get(key))) + + +def _resolve_key_based_on_device_class(member_entry: ConfigEntry, device_class: SensorDeviceClass) -> str | None: + """Resolve the correct key for power/energy sensor based on device class.""" + if member_entry.data.get(CONF_SENSOR_TYPE) == SensorType.REAL_POWER: + return CONF_ENTITY_ID if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY + return ENTRY_DATA_POWER_ENTITY if device_class == SensorDeviceClass.POWER else ENTRY_DATA_ENERGY_ENTITY + + +def _add_specified_sensors(entry: ConfigEntry, device_class: SensorDeviceClass, resolved_ids: set[str]) -> None: + """Add additional power/energy sensors specified by the user.""" + conf_key = CONF_GROUP_POWER_ENTITIES if device_class == SensorDeviceClass.POWER else CONF_GROUP_ENERGY_ENTITIES + resolved_ids.update(entry.data.get(conf_key) or []) + + +async def _add_include_based_sensors( + hass: HomeAssistant, + entry: ConfigEntry, + device_class: SensorDeviceClass, + resolved_ids: set[str], +) -> None: + """Add entities from the defined areas, devices and floors.""" + if all(k not in entry.data for k in (CONF_AREA, CONF_FLOOR, CONF_GROUP_MEMBER_DEVICES)): + return + + result = await find_entities( + hass, + await build_entity_include_filter(hass, entry), + bool(entry.data.get(CONF_INCLUDE_NON_POWERCALC_SENSORS)), + ) + resolved_ids.update(filter_entity_list_by_class(result.resolved, device_class)) + + +async def _add_subgroup_entities( + hass: HomeAssistant, + entry: ConfigEntry, + device_class: SensorDeviceClass, + resolved_ids: set[str], +) -> None: + """Recursively add entities from subgroups.""" + subgroups = entry.data.get(CONF_SUB_GROUPS) + if not subgroups: + return + + for subgroup_entry_id in subgroups: + subgroup_entry = hass.config_entries.async_get_entry(subgroup_entry_id) + if subgroup_entry is None: + _LOGGER.error("Subgroup config entry not found: %s", subgroup_entry_id) + continue + + await resolve_entity_ids_recursively(hass, subgroup_entry, device_class, resolved_ids) + + @callback def create_grouped_power_sensor( hass: HomeAssistant, @@ -448,11 +476,17 @@ class GroupedSensor(BaseEntity, SensorEntity): self._entities = entities self._sensor_config = sensor_config if self._is_energy_sensor: - self._rounding_digits = int(sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION)) - self._update_interval: int = int(sensor_config.get(CONF_GROUP_ENERGY_UPDATE_INTERVAL, DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL)) + self._rounding_digits = int( + sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION), + ) + self._update_interval: int = int( + sensor_config.get(CONF_GROUP_ENERGY_UPDATE_INTERVAL, DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL), + ) else: self._rounding_digits = int(sensor_config.get(CONF_POWER_SENSOR_PRECISION, DEFAULT_POWER_SENSOR_PRECISION)) - self._update_interval = int(sensor_config.get(CONF_GROUP_POWER_UPDATE_INTERVAL, DEFAULT_GROUP_POWER_UPDATE_INTERVAL)) + self._update_interval = int( + sensor_config.get(CONF_GROUP_POWER_UPDATE_INTERVAL, DEFAULT_GROUP_POWER_UPDATE_INTERVAL), + ) self._attr_suggested_display_precision = self._rounding_digits if unique_id: self._attr_unique_id = unique_id @@ -523,7 +557,11 @@ class GroupedSensor(BaseEntity, SensorEntity): domain = self._sensor_config.get(CONF_DOMAIN) if domain == CONF_ALL: entity_registry = er.async_get(self.hass) - entities = {entity.entity_id for entity in entity_registry.entities.values() if entity.device_class == self.device_class} + entities = { + entity.entity_id + for entity in entity_registry.entities.values() + if entity.device_class == self.device_class + } else: entities = self.hass.data[DOMAIN].get(DATA_DOMAIN_ENTITIES).get(domain, []) entities = filter_entity_list_by_class( @@ -533,7 +571,7 @@ class GroupedSensor(BaseEntity, SensorEntity): excluded_entities = self._sensor_config.get(CONF_EXCLUDE_ENTITIES) or [] self._entities = set({entity for entity in entities if entity not in excluded_entities}) - async def on_start(self, _: Any) -> None: # noqa + async def on_start(self, _: HomeAssistant) -> None: """Initialize group sensor when HA is starting.""" await self.init_domain_group() @@ -557,7 +595,9 @@ class GroupedSensor(BaseEntity, SensorEntity): """Initial update for the group sensor state.""" all_states = [self.hass.states.get(entity_id) for entity_id in self._entities] states: list[State] = list(filter(None, all_states)) - available_states = [state for state in states if state and state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE]] + available_states = [ + state for state in states if state and state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] if not available_states and not self._ignore_unavailable_state: new_state: Decimal | str = STATE_UNAVAILABLE else: @@ -652,6 +692,37 @@ class GroupedSensor(BaseEntity, SensorEntity): def get_group_entities(self) -> dict[str, set[str]]: return {ATTR_ENTITIES: self._entities} + def debug_group(self) -> dict[str, Any]: + members: dict[str, dict[str, str | None]] = {} + for entity_id in sorted(self._entities): + members[entity_id] = self._get_member_debug_info(entity_id) + + return { + ATTR_STATE: str(self.state), + ATTR_UNIT_OF_MEASUREMENT: self.native_unit_of_measurement, + ATTR_MEMBERS: members, + } + + def _get_member_debug_info(self, entity_id: str) -> dict[str, str | None]: + state = self.hass.states.get(entity_id) + if state is None: + return { + ATTR_STATE: None, + ATTR_UNIT_OF_MEASUREMENT: None, + } + + if state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + return { + ATTR_STATE: str(state.state), + ATTR_UNIT_OF_MEASUREMENT: state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), + } + + converted_value = round(self._get_state_value_in_native_unit(state), self._rounding_digits) + return { + ATTR_STATE: str(converted_value), + ATTR_UNIT_OF_MEASUREMENT: self.native_unit_of_measurement, + } + @abstractmethod def calculate_initial_state( self, @@ -681,7 +752,9 @@ class GroupedPowerSensor(GroupedSensor, PowerSensor): member_available_states: list[State], member_states: list[State], ) -> Decimal | str: - self._member_states = {state.entity_id: self._get_state_value_in_native_unit(state) for state in member_available_states} + self._member_states = { + state.entity_id: self._get_state_value_in_native_unit(state) for state in member_available_states + } return self.get_summed_state() def calculate_new_state(self, state: State) -> Decimal | str: @@ -776,8 +849,9 @@ class GroupedEnergySensor(GroupedSensor, RestoreSensor, EnergySensor): member_available_states: list[State], member_states: list[State], ) -> Decimal: - """Calculate the new group energy sensor state - For each member sensor we calculate the delta by looking at the previous known state and compare it to the current. + """Calculate the new group energy sensor state. + + For each member, calculate the delta between the previous known state and the current. """ group_sum = Decimal(self._native_value_exact) if self._native_value_exact else Decimal(0) _LOGGER.debug("%s: Recalculate, current value: %s", self.entity_id, group_sum) @@ -830,10 +904,7 @@ class GroupedEnergySensor(GroupedSensor, RestoreSensor, EnergySensor): ) start_at_zero = self._sensor_config.get(CONF_GROUP_ENERGY_START_AT_ZERO, True) - if prev_state is None and start_at_zero: # noqa: SIM108 - delta = Decimal(0) - else: - delta = cur_value - prev_value + delta = Decimal(0) if prev_state is None and start_at_zero else cur_value - prev_value if _LOGGER.isEnabledFor(logging.DEBUG): # pragma: no cover _LOGGER.debug( @@ -890,7 +961,9 @@ class PreviousStateStore: _LOGGER.debug("Load previous energy sensor states from store") stored_states = await instance.store.async_load() or {} for group, entities in stored_states.items(): - instance.states[group] = {entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items()} + instance.states[group] = { + entity_id: State.from_dict(json_state) for (entity_id, json_state) in entities.items() + } except HomeAssistantError as exc: # pragma: no cover _LOGGER.error("Error loading previous energy sensor states", exc_info=exc) @@ -943,7 +1016,7 @@ class PreviousStateStore: def async_setup_dump(self) -> None: """Set up the listeners for persistence.""" - async def _async_dump_states(*_: Any) -> None: # noqa: ANN401 + async def _async_dump_states(*_: object) -> None: await self.persist_states() # Dump states periodically @@ -951,9 +1024,10 @@ class PreviousStateStore: self.hass, _async_dump_states, STATE_DUMP_INTERVAL, + cancel_on_shutdown=True, ) - async def _async_dump_states_at_stop(*_: Any) -> None: # noqa: ANN401 + async def _async_dump_states_at_stop(*_: object) -> None: cancel_interval() await self.persist_states() diff --git a/custom_components/powercalc/sensors/group/domain.py b/custom_components/powercalc/sensors/group/domain.py index db2a82a..80301cb 100644 --- a/custom_components/powercalc/sensors/group/domain.py +++ b/custom_components/powercalc/sensors/group/domain.py @@ -7,7 +7,7 @@ from custom_components.powercalc.const import CONF_GROUP_TYPE, GroupType from custom_components.powercalc.sensors.group.custom import create_group_sensors_custom -async def create_domain_group_sensor( +def create_domain_group_sensor( hass: HomeAssistant, config: ConfigType, ) -> list[Entity]: @@ -16,7 +16,7 @@ async def create_domain_group_sensor( if CONF_UNIQUE_ID not in config: config[CONF_UNIQUE_ID] = generate_unique_id(config) config[CONF_GROUP_TYPE] = GroupType.DOMAIN - return await create_group_sensors_custom( + return create_group_sensors_custom( hass, name, config, diff --git a/custom_components/powercalc/sensors/group/factory.py b/custom_components/powercalc/sensors/group/factory.py index bdead60..612bd61 100644 --- a/custom_components/powercalc/sensors/group/factory.py +++ b/custom_components/powercalc/sensors/group/factory.py @@ -24,12 +24,12 @@ async def create_group_sensors( collect_analytics(hass, config_entry).inc(DATA_GROUP_TYPES, group_type) if group_type == GroupType.DOMAIN: - return await domain_group.create_domain_group_sensor( + return domain_group.create_domain_group_sensor( hass, sensor_config, ) if group_type == GroupType.STANDBY: - return await standby_group.create_general_standby_sensors(hass, sensor_config) + return standby_group.create_general_standby_sensors(hass, sensor_config) if group_type == GroupType.CUSTOM: if config_entry: @@ -38,14 +38,14 @@ async def create_group_sensors( entry=config_entry, sensor_config=sensor_config, ) - return await custom_group.create_group_sensors_yaml( + return custom_group.create_group_sensors_yaml( hass=hass, sensor_config=sensor_config, entities=entities or [], ) if group_type == GroupType.SUBTRACT: - return await subtract_group.create_subtract_group_sensors( + return subtract_group.create_subtract_group_sensors( hass=hass, config=sensor_config, ) diff --git a/custom_components/powercalc/sensors/group/standby.py b/custom_components/powercalc/sensors/group/standby.py index 9bb800f..9c46372 100644 --- a/custom_components/powercalc/sensors/group/standby.py +++ b/custom_components/powercalc/sensors/group/standby.py @@ -30,7 +30,7 @@ from custom_components.powercalc.sensors.power import PowerSensor _LOGGER = logging.getLogger(__name__) -async def create_general_standby_sensors( +def create_general_standby_sensors( hass: HomeAssistant, config: ConfigType, ) -> list[Entity]: @@ -44,8 +44,8 @@ async def create_general_standby_sensors( power_sensor.entity_id = "sensor.all_standby_power" sensor_config = config.copy() sensor_config[CONF_NAME] = "All standby" - source_entity = await create_source_entity(DUMMY_ENTITY_ID, hass) - energy_sensor = await create_energy_sensor( + source_entity = create_source_entity(DUMMY_ENTITY_ID, hass) + energy_sensor = create_energy_sensor( hass, sensor_config, power_sensor, @@ -80,8 +80,8 @@ class StandbyPowerSensor(SensorEntity, PowerSensor): """Calculate sum of all power sensors in standby, and update the state of the sensor.""" if self.standby_sensors: self._attr_native_value = Decimal( - round( # type: ignore - sum(self.standby_sensors.values()), + round( + sum(self.standby_sensors.values(), Decimal(0)), self._rounding_digits, ), ) diff --git a/custom_components/powercalc/sensors/group/subtract.py b/custom_components/powercalc/sensors/group/subtract.py index 3cf6b76..7366d98 100644 --- a/custom_components/powercalc/sensors/group/subtract.py +++ b/custom_components/powercalc/sensors/group/subtract.py @@ -24,7 +24,7 @@ from custom_components.powercalc.sensors.utility_meter import create_utility_met _LOGGER = logging.getLogger(__name__) -async def create_subtract_group_sensors( +def create_subtract_group_sensors( hass: HomeAssistant, config: ConfigType, ) -> list[Entity]: @@ -58,7 +58,7 @@ async def create_subtract_group_sensors( ) sensors.append(power_sensor) if config.get(CONF_CREATE_ENERGY_SENSORS): - energy_sensor = await create_energy_sensor( + energy_sensor = create_energy_sensor( hass, config, power_sensor, @@ -67,7 +67,7 @@ async def create_subtract_group_sensors( config[CONF_UTILITY_METER_NET_CONSUMPTION] = True sensors.extend( - await create_utility_meters( + create_utility_meters( hass, energy_sensor, config, diff --git a/custom_components/powercalc/sensors/group/tracked_untracked.py b/custom_components/powercalc/sensors/group/tracked_untracked.py index bc485b2..4bb0a6d 100644 --- a/custom_components/powercalc/sensors/group/tracked_untracked.py +++ b/custom_components/powercalc/sensors/group/tracked_untracked.py @@ -2,7 +2,6 @@ from __future__ import annotations from enum import StrEnum import logging -from typing import Any from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry @@ -53,7 +52,11 @@ async def find_auto_tracked_power_entities(hass: HomeAssistant, exclude_entities if exclude_entities: entity_filter = LambdaFilter(lambda entity: entity.entity_id not in exclude_entities) result = await find_entities(hass, entity_filter) - return {entity.entity_id for entity in result.resolved if isinstance(entity, PowerSensor) and not isinstance(entity, GroupedSensor)} + return { + entity.entity_id + for entity in result.resolved + if isinstance(entity, PowerSensor) and not isinstance(entity, GroupedSensor) + } class TrackedPowerSensorFactory: @@ -67,7 +70,9 @@ class TrackedPowerSensorFactory: """Create tracked/untracked group sensors.""" unique_id = str(self.config.get(CONF_UNIQUE_ID)) - main_power_sensor = str(self.config.get(CONF_MAIN_POWER_SENSOR)) if self.config.get(CONF_MAIN_POWER_SENSOR) else None + main_power_sensor = ( + str(self.config.get(CONF_MAIN_POWER_SENSOR)) if self.config.get(CONF_MAIN_POWER_SENSOR) else None + ) self.config[CONF_DISABLE_EXTENDED_ATTRIBUTES] = True # prevent adding all entities in the state attributes self.tracked_entities = await self.get_tracked_power_entities() @@ -83,7 +88,7 @@ class TrackedPowerSensorFactory: energy_sensor = await self.create_energy_sensor(SensorType.TRACKED, tracked_sensor) entities.append(energy_sensor) entities.extend( - await create_utility_meters( + create_utility_meters( self.hass, energy_sensor, {CONF_UTILITY_METER_NET_CONSUMPTION: True, **self.config}, @@ -102,7 +107,7 @@ class TrackedPowerSensorFactory: energy_sensor = await self.create_energy_sensor(SensorType.UNTRACKED, untracked_sensor) entities.append(energy_sensor) entities.extend( - await create_utility_meters( + create_utility_meters( self.hass, energy_sensor, {CONF_UTILITY_METER_NET_CONSUMPTION: True, **self.config}, @@ -116,12 +121,15 @@ class TrackedPowerSensorFactory: Get all power entities which are part of the tracked sensor group """ if not bool(self.config.get(CONF_GROUP_TRACKED_AUTO, False)): - return set(self.config.get(CONF_GROUP_TRACKED_POWER_ENTITIES)) # type: ignore + tracked_entities: list[str] | None = self.config.get(CONF_GROUP_TRACKED_POWER_ENTITIES) + if not isinstance(tracked_entities, list): + return set() + return set(tracked_entities) # For auto mode, we also want to listen for any changes in the entity registry # Dynamically add/remove power sensors from the tracked group @callback - def _start_entity_registry_listener(_: Any) -> None: # noqa ANN401 + def _start_entity_registry_listener(_: Event) -> None: self.hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _start_entity_registry_listener) @@ -137,8 +145,9 @@ class TrackedPowerSensorFactory: entity_id = event.data["entity_id"] action = event.data["action"] - if action == "update" and "old_entity_id" in event.data: - if event.data["old_entity_id"] in self.tracked_entities: # type: ignore + old_entity_id = event.data.get("old_entity_id") + if action == "update" and old_entity_id is not None: + if old_entity_id in self.tracked_entities: return await self.reload() return None # pragma: no cover diff --git a/custom_components/powercalc/sensors/power.py b/custom_components/powercalc/sensors/power.py index a209286..93e27d0 100644 --- a/custom_components/powercalc/sensors/power.py +++ b/custom_components/powercalc/sensors/power.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable, Coroutine from copy import copy from datetime import datetime, timedelta from decimal import Decimal @@ -25,6 +26,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Event, + HassJob, HomeAssistant, State, callback, @@ -37,6 +39,7 @@ import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import ( EventStateChangedData, TrackTemplate, + TrackTemplateResult, async_call_later, async_track_state_change_event, async_track_template_result, @@ -122,7 +125,7 @@ async def create_power_sensor( """Create the power sensor based on powercalc sensor configuration.""" if CONF_POWER_SENSOR_ID in sensor_config: # Use an existing power sensor, only create energy sensors / utility meters - return await create_real_power_sensor(hass, sensor_config) + return create_real_power_sensor(hass, sensor_config) return await create_virtual_power_sensor( hass, @@ -147,7 +150,11 @@ async def create_virtual_power_sensor( if CONF_CALCULATION_ENABLED_CONDITION not in sensor_config and power_profile.calculation_enabled_condition: sensor_config[CONF_CALCULATION_ENABLED_CONDITION] = power_profile.calculation_enabled_condition - if config_entry and await power_profile.requires_manual_sub_profile_selection and "/" not in sensor_config.get(CONF_MODEL, ""): + if ( + config_entry + and await power_profile.requires_manual_sub_profile_selection + and "/" not in sensor_config.get(CONF_MODEL, "") + ): ir.async_create_issue( hass, DOMAIN, @@ -181,10 +188,14 @@ async def create_virtual_power_sensor( a = collect_analytics(hass, config_entry) a.inc(DATA_STRATEGIES, strategy) a.add(DATA_POWER_PROFILES, power_profile) - a.inc(DATA_POWER_PROFILE_SOURCES, power_profile.configuration_source if power_profile else PowerProfileSource.MANUAL) + a.inc( + DATA_POWER_PROFILE_SOURCES, + power_profile.configuration_source if power_profile else PowerProfileSource.MANUAL, + ) _LOGGER.debug( - "Creating power sensor (entity_id=%s entity_category=%s, sensor_name=%s strategy=%s manufacturer=%s model=%s unique_id=%s)", + "Creating power sensor (entity_id=%s entity_category=%s, sensor_name=%s strategy=%s " + "manufacturer=%s model=%s unique_id=%s)", source_entity.entity_id, entity_category, name, @@ -313,7 +324,7 @@ def _get_standby_power( return standby_power, standby_power_on -async def create_real_power_sensor( +def create_real_power_sensor( hass: HomeAssistant, sensor_config: dict, ) -> RealPowerSensor: @@ -450,7 +461,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): new_state, ) - async def template_change_listener(*_: Any) -> None: # noqa: ANN401 + async def template_change_listener(*_: object) -> None: """Handle for state changes for referenced templates.""" state = self.hass.states.get(self._source_entity.entity_id) await self._handle_source_entity_state_change( @@ -461,9 +472,9 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): async def initial_update(hass: HomeAssistant) -> None: """Calculate initial value and push state""" - # When using reload service energy sensor became unavailable - # This is caused because state change listener of energy sensor is registered before power sensor pushes initial update - # Adding sleep 0 fixes this issue. + # When using reload service the energy sensor became unavailable. + # This is caused because the state change listener of the energy sensor is registered + # before the power sensor pushes its initial update. Adding sleep 0 fixes this issue. await asyncio.sleep(0) if self._strategy_instance: await self._strategy_instance.on_start(hass) @@ -472,7 +483,9 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): if (not entities and self._source_entity.entity_id == DUMMY_ENTITY_ID) or not entities: entities.add(DUMMY_ENTITY_ID) for entity_id in entities: - new_state = self.hass.states.get(entity_id) if entity_id != DUMMY_ENTITY_ID else State(entity_id, STATE_ON) + new_state = ( + self.hass.states.get(entity_id) if entity_id != DUMMY_ENTITY_ID else State(entity_id, STATE_ON) + ) await self._handle_source_entity_state_change( entity_id, new_state, @@ -480,30 +493,56 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): # Add listeners for all tracking entities and templates. entities_to_track = self._get_tracking_entities() - - self._track_entities = {e for e in entities_to_track if isinstance(e, str)} - self.async_on_remove( - async_track_state_change_event(self.hass, self._track_entities, appliance_state_listener), - ) - - track_templates: list[TrackTemplate] = [e for e in entities_to_track if isinstance(e, TrackTemplate)] - if track_templates: - async_track_template_result(self.hass, track_templates=track_templates, action=template_change_listener) + self._register_tracking_listeners(entities_to_track, appliance_state_listener, template_change_listener) # Trigger initial update self.async_on_remove(start.async_at_start(self.hass, initial_update)) - if hasattr(self._strategy_instance, "set_update_callback"): - self._strategy_instance.set_update_callback(self._update_power_sensor) + self._strategy_instance.set_update_callback(self._update_power_sensor) + self._register_force_update_interval() + + def _register_tracking_listeners( + self, + entities_to_track: list[str | TrackTemplate], + appliance_state_listener: Callable[[Event[EventStateChangedData]], Awaitable[None]], + template_change_listener: Callable[ + [Event[EventStateChangedData] | None, list[TrackTemplateResult]], + Coroutine[Any, Any, None] | None, + ], + ) -> None: + self._track_entities = {e for e in entities_to_track if isinstance(e, str)} + self.async_on_remove( + async_track_state_change_event(self.hass, self._track_entities, appliance_state_listener), + ) + track_templates: list[TrackTemplate] = [e for e in entities_to_track if isinstance(e, TrackTemplate)] + if track_templates: + template_tracker = async_track_template_result( + self.hass, + track_templates=track_templates, + action=template_change_listener, + ) + self.async_on_remove( + template_tracker.async_remove, + ) + + def _register_force_update_interval(self) -> None: force_update_interval = self._sensor_config.get(CONF_POWER_UPDATE_INTERVAL, 0) - if force_update_interval > 0: + if force_update_interval <= 0: + return - @callback - def async_update(__: datetime | None = None) -> None: - self.async_schedule_update_ha_state(True) + @callback + def async_update(__: datetime | None = None) -> None: + self.async_schedule_update_ha_state(True) - async_track_time_interval(self.hass, async_update, timedelta(seconds=force_update_interval)) + self.async_on_remove( + async_track_time_interval( + self.hass, + async_update, + timedelta(seconds=force_update_interval), + cancel_on_shutdown=True, + ), + ) def _get_tracking_entities(self) -> list[str | TrackTemplate]: """Return entities and templates that should be tracked.""" @@ -616,16 +655,8 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): """Calculate power consumption using configured strategy.""" assert self._strategy_instance is not None - # Resolve the relevant entity state - entity_state = state - if self._source_entity.entity_id == DUMMY_ENTITY_ID and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH: - if self._availability_entity and state.entity_id == self._availability_entity: - entity_state = State(DUMMY_ENTITY_ID, STATE_ON) - elif ( - self._calculation_strategy != CalculationStrategy.MULTI_SWITCH - and state.entity_id != self._source_entity.entity_id - and (entity_state := self.hass.states.get(self._source_entity.entity_id)) is None - ): + entity_state = self._resolve_calculation_state(state) + if entity_state is None: return None # Handle unavailable power @@ -633,31 +664,56 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): if entity_state.state == STATE_UNAVAILABLE and unavailable_power is not None: return Decimal(unavailable_power) - # Handle standby power - standby_power = None - if entity_state.state in self._off_states or not await self.is_calculation_enabled(entity_state): - if isinstance(self._strategy_instance, PlaybookStrategy): - await self._strategy_instance.stop_playbook() - standby_power = await self.calculate_standby_power(entity_state) - self._standby_sensors[self.entity_id] = standby_power - - if self._strategy_instance.can_calculate_standby() or self._calculation_strategy != CalculationStrategy.MULTI_SWITCH: - return standby_power + standby_power = await self._calculate_state_standby_power(entity_state) + if standby_power is not None and ( + self._strategy_instance.can_calculate_standby() + or self._calculation_strategy != CalculationStrategy.MULTI_SWITCH + ): + return standby_power # Calculate actual power using configured strategy power = await self._strategy_instance.calculate(entity_state) if power is None: return None - # Add standby power if available + return Decimal(self._apply_power_adjustments(power, standby_power)) + + def _resolve_calculation_state(self, state: State) -> State | None: + if ( + self._source_entity.entity_id == DUMMY_ENTITY_ID + and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH + ): + if self._availability_entity and state.entity_id == self._availability_entity: + return State(DUMMY_ENTITY_ID, STATE_ON) + return state + + if ( + self._calculation_strategy == CalculationStrategy.MULTI_SWITCH + or state.entity_id == self._source_entity.entity_id + ): + return state + + return cast(State | None, self.hass.states.get(self._source_entity.entity_id)) + + async def _calculate_state_standby_power(self, entity_state: State) -> Decimal | None: + if entity_state.state not in self._off_states and await self.is_calculation_enabled(entity_state): + return None + + assert self._strategy_instance is not None + if isinstance(self._strategy_instance, PlaybookStrategy): + await self._strategy_instance.stop_playbook() + + standby_power = await self.calculate_standby_power(entity_state) + self._standby_sensors[self.entity_id] = standby_power + return standby_power + + def _apply_power_adjustments(self, power: Decimal, standby_power: Decimal | None) -> Decimal: if standby_power: power += standby_power - # Apply multiply factor to power if self._multiply_factor: power *= Decimal(self._multiply_factor) - # Add standby power-on adjustments if applicable if self._standby_power_on and not standby_power: additional_standby_power = self._standby_power_on self._standby_sensors[self.entity_id] = self._standby_power_on @@ -665,7 +721,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): additional_standby_power *= Decimal(self._multiply_factor) power += additional_standby_power - return Decimal(power) + return power async def _switch_sub_profile_dynamically(self, state: State) -> None: """Dynamically select a different sub profile depending on the entity state or attributes @@ -698,7 +754,7 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): delay = sleep_power.get(CONF_DELAY) or 0 @callback - def _update_sleep_power(*_: Any) -> None: # noqa: ANN401 + def _update_sleep_power(*_: object) -> None: power = Decimal(sleep_power.get(CONF_POWER) or 0) if self._multiply_factor_standby and self._multiply_factor: power *= Decimal(self._multiply_factor) @@ -707,14 +763,14 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): self._sleep_power_timer = async_call_later( self.hass, delay, - _update_sleep_power, + HassJob(_update_sleep_power, name=f"{self.entity_id} sleep power", cancel_on_shutdown=True), ) standby_power = self._standby_power if self._strategy_instance.can_calculate_standby(): standby_power = await self._strategy_instance.calculate(state) or self._standby_power - evaluated = await evaluate_power(standby_power) + evaluated = evaluate_power(standby_power) if evaluated is None: evaluated = Decimal(0) standby_power = evaluated @@ -775,9 +831,22 @@ class VirtualPowerSensor(SensorEntity, PowerSensor): raise HomeAssistantError("supported only playbook enabled sensors") return self._strategy_instance + async def async_will_remove_from_hass(self) -> None: + """Cancel outstanding timers when the entity is removed.""" + if self._sleep_power_timer is not None: + self._sleep_power_timer() + self._sleep_power_timer = None + if isinstance(self._strategy_instance, PlaybookStrategy): + await self._strategy_instance.stop_playbook() + await super().async_will_remove_from_hass() + async def async_switch_sub_profile(self, profile: str) -> None: """Switches to a new sub profile""" - if not self._power_profile or not await self._power_profile.has_sub_profiles or self._power_profile.sub_profile_select: + if ( + not self._power_profile + or not await self._power_profile.has_sub_profiles + or self._power_profile.sub_profile_select + ): raise HomeAssistantError( "This is only supported for sensors having sub profiles, and no automatic profile selection", ) diff --git a/custom_components/powercalc/sensors/utility_meter.py b/custom_components/powercalc/sensors/utility_meter.py index 3a9a2ff..8d51276 100644 --- a/custom_components/powercalc/sensors/utility_meter.py +++ b/custom_components/powercalc/sensors/utility_meter.py @@ -41,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) GENERAL_TARIFF = "general" -async def create_utility_meters( +def create_utility_meters( hass: HomeAssistant, energy_sensor: EnergySensor, sensor_config: dict, @@ -62,7 +62,7 @@ async def create_utility_meters( unique_id = f"{energy_sensor.unique_id}_{meter_type}" if energy_sensor.unique_id else None if should_create_utility_meter(hass, unique_id, energy_sensor): utility_meters.extend( - await create_meters_for_type( + create_meters_for_type( hass, energy_sensor, sensor_config, @@ -98,7 +98,7 @@ def should_create_utility_meter( return not (existing_entity_id and hass.states.get(existing_entity_id)) # pragma: no cover -async def create_meters_for_type( +def create_meters_for_type( hass: HomeAssistant, energy_sensor: EnergySensor, sensor_config: dict, @@ -116,7 +116,7 @@ async def create_meters_for_type( # Create generic utility meter if not tariffs or GENERAL_TARIFF in tariffs: - utility_meter = await create_utility_meter( + utility_meter = create_utility_meter( hass, energy_sensor.entity_id, entity_id, @@ -130,7 +130,7 @@ async def create_meters_for_type( # Create tariff-specific utility meters if tariffs: - new_tariff_sensors = await create_tariff_meters( + new_tariff_sensors = create_tariff_meters( hass, energy_sensor, entity_id, @@ -148,7 +148,7 @@ async def create_meters_for_type( return utility_meters -async def create_tariff_meters( +def create_tariff_meters( hass: HomeAssistant, energy_sensor: EnergySensor, entity_id: str, @@ -161,11 +161,11 @@ async def create_tariff_meters( ) -> list[VirtualUtilityMeter]: """Create utility meters for specific tariffs.""" filtered_tariffs = [t for t in tariffs if t != GENERAL_TARIFF] - tariff_select = await create_tariff_select(config_entry, filtered_tariffs, hass, name, unique_id) + tariff_select = create_tariff_select(config_entry, filtered_tariffs, hass, name, unique_id) tariff_sensors = [] for tariff in filtered_tariffs: - utility_meter = await create_utility_meter( + utility_meter = create_utility_meter( hass, energy_sensor.entity_id, entity_id, @@ -181,7 +181,7 @@ async def create_tariff_meters( return tariff_sensors -async def create_tariff_select( +def create_tariff_select( config_entry: ConfigEntry | None, tariffs: list, hass: HomeAssistant, @@ -215,7 +215,7 @@ async def create_tariff_select( return tariff_select -async def create_utility_meter( +def create_utility_meter( hass: HomeAssistant, source_entity: str, entity_id: str, @@ -258,7 +258,9 @@ async def create_utility_meter( params = {key: value for key, value in params.items() if key in signature.parameters} utility_meter = VirtualUtilityMeter(**params) # type: ignore[no-untyped-call] - utility_meter.rounding_digits = int(sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION)) + utility_meter.rounding_digits = int( + sensor_config.get(CONF_ENERGY_SENSOR_PRECISION, DEFAULT_ENERGY_SENSOR_PRECISION), + ) utility_meter.entity_id = entity_id return utility_meter @@ -280,8 +282,10 @@ class VirtualUtilityMeter(UtilityMeterSensor, BaseEntity): @property def native_value(self) -> StateType | Decimal: """Return the state of the sensor.""" - value = self._state if hasattr(self, "_state") else self._attr_native_value # pre HA 2024.12 value was stored in _state + value = ( + self._state if hasattr(self, "_state") else self._attr_native_value + ) # pre HA 2024.12 value was stored in _state if self.rounding_digits and value is not None: - return Decimal(round(value, self.rounding_digits)) # type: ignore + return Decimal(round(value, self.rounding_digits)) # type: ignore[arg-type] - return value # type: ignore + return value # type: ignore[return-value] diff --git a/custom_components/powercalc/service/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/service/__pycache__/__init__.cpython-314.pyc index 75a0ead8352c458decc757134e507319507aa2ae..80bba73bbb521524ca3ad993a7528dc7b59b3e08 100644 GIT binary patch delta 19 ZcmbQmIE#^6n~#@^0SHdN(4WZN4*(|m1fKu^ delta 19 ZcmbQmIE#^6n~#@^0SI!IsZZqY2LK~h1NHy_ diff --git a/custom_components/powercalc/service/__pycache__/gui_configuration.cpython-314.pyc b/custom_components/powercalc/service/__pycache__/gui_configuration.cpython-314.pyc index 895b8540f3a0fe466f18d3597be099f9bcba20c0..37560589c133ce212de78a6e2b5a9dbc68a4e525 100644 GIT binary patch delta 20 acmaDX_*jrzn~#@^0SHdN(BH^?g989L$pyjy delta 20 acmaDX_*jrzn~#@^0SI!Isc+=I!2tj@d<7W* diff --git a/custom_components/powercalc/services.yaml b/custom_components/powercalc/services.yaml index db78c82..b648f97 100644 --- a/custom_components/powercalc/services.yaml +++ b/custom_components/powercalc/services.yaml @@ -70,6 +70,13 @@ get_group_entities: entity: domain: sensor integration: powercalc +debug_group: + name: Debug group + description: Get a debug overview of a Powercalc power or energy group including current member values + target: + entity: + domain: sensor + integration: powercalc increase_daily_energy: name: Increase daily energy sensor description: Increases the sensor with a given amount diff --git a/custom_components/powercalc/strategy/__pycache__/__init__.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/__init__.cpython-314.pyc index 61eaf0cfb8b8d7075fed423e17230e07ce34554d..11d7459c84fbe5efa619b1e56d55dbeec491e24d 100644 GIT binary patch delta 19 ZcmbQuIGd4Mn~#@^0SHdN(4WXX0RSf-1fl={ delta 19 ZcmbQuIGd4Mn~#@^0SI!IsZZpd001L<1Ni^| diff --git a/custom_components/powercalc/strategy/__pycache__/composite.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/composite.cpython-314.pyc index b8c440835f93a47b95426f21ae43ce163b0cf55e..8626c406243a448bf8660f644bc3b65a284e5aad 100644 GIT binary patch delta 1527 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 diff --git a/custom_components/powercalc/strategy/__pycache__/factory.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/factory.cpython-314.pyc index e986cb1305dbd078441d676d15c46f57270ca092..13c289e2000f8faa2c8e19f1425292dd7fd3e580 100644 GIT binary patch delta 540 zcmeCI_+7!L&Bx2d00gIB=x43c*vQwZ?4{3Y|B(SmJP?rjzzU>4urWxed|(H$I2a_f zKX8IrTnt>&AGv|7B6FbbWEPMaP{0PHn;955KYyG2P!UR!Nz~;*o!cxFCSwU4?olPC8Go6QlL53lQ9cEAviwPry zoxvE!HrYZoTE5kU86u+0z|P>KP|76FP|8%mn8{$uoXM!kI(d`oYsPJx=cyGk11SM5 zen!J8eZPE#;*z4olGOCf)MAC?{JgZx^wOf#6os_>A|N-nAip@XBo(HnaszT(oPc!b``)3g&9uTTE2?aKOvjX^+WvWt$c&H@sQVg#{GSb^-xT?+A&BXm_6FKw>V&1D8sZw%ZR8DDKS xHu?voCL5W&Vf?;X-t-wGVQTsJ%^Ad{?X2Eb~#vsPe3}6Zzp#T>1kR1R3 delta 417 zcmexe(OJQ#&Bx2d00cS9)U*DmZ{+J#_R?mx|HuF&9tcQ%UKKCpvW91IfL zA2>lQE(R{?kK903kvULzG7HEIC}0E9%?u2jpU+HwsJv7lgr$Hrh65&9z&3f3iaMjg zAZBrf9F81;0`@T0$w{iwjBS(m zsme04OunP~nsLkK6KaLbK*~yspK)@Pc9JMxacNR~aY<2PNosm#YVj?W-29Z($se^7 z7>`X3(sAW@AgS5GcZXkk@*EvqiOaH19e&?gL^&fEKQVyFk9rIqjA4wEzv@_+n*d$Y zs_3l3xLgoO>~u5%l81Epo#j~$*)TfGaviee0J0CWN&v+V%QAx4@~qA(le2VN8P9G0 zpqtAKq~Z+S7#W{!UTX9YNKIa1@`myA=82}y7#Tlpt}vg;2nye?QJcjqLzo4FnHhr^ LKQn+SaHs+Rc&2co diff --git a/custom_components/powercalc/strategy/__pycache__/fixed.cpython-314.pyc b/custom_components/powercalc/strategy/__pycache__/fixed.cpython-314.pyc index 40ee407b667d70748a85766424ba1c9e3819c118..eb58b3b8aa273fb0ce9f4c8b7a0ce8535ea5d3b9 100644 GIT binary patch delta 884 zcmZ8f%}*0i5TCbY+ivN$TWF=yZWmgrw8cVkAt`M@C18k&K|(4qMAMYjKx?VnYS55K z_zO&)iS>jB6EX3isW%f7HC}{65lnUUpf`;NX+RC(;@jmT!8!b9-n{wvy*G0&v=(AR z7S;qrmcF-7Ii9g;8a*!Eq$9{4szMzdS{ztc)zhqY!Y}nML}5kWgx=w8eHV)2h(Sg^ z+-q>+WrGKe;h%;l_>pmh^Ffzx5(TKU3YrZtvJd+ik@b>B0lnNpl-!ChGZ)bZ{FZ5@ z8vOVde)Y4xXtldVO+D7)Nz^c%U&#^(&QkrFlP@9Dosz9K8^n|8m4+%*j0()X0 zh7N$;Xv9^^b6*6&4GA=?mikXds0K*Dun8k`Jzlc98aWVmL&V*R0!j5jK!-D2u=_w8 zJ&8OHfHpG3@PYtyFbab~fE*g%r;|YA6n0yk*v*-y$LYQP2{ZyDAQG0U%)lHiz?n7x z20X;S^YwBQxeDkM(`0@!n=d4@DM@CCOE#*0N}<9XvV)j;iolU3*+rDX#IHzMX(lK4 zD&5#-^W6@|Gt;SLCf^(0zd5fawG;S3D6d0}&+?6r-5K~&&ljbKW0m^wCJna8UA8Da z7+n-ATt}Jf_{_()@@nIGr-&9DA_^GgljJCCqf%)p`eZIAWhXRx+VB*;G}BoiE=7MM znYk|I1C+KoinSmoQ|U_u>1r;MEJ*nSFK}SN=D=245M9GPwo?Ni&8~{syJGfMRi6zKNLQrZ&IrXZn(Mo6)m5)WvIi6~a+p->6gR-%bU zu8hP{A=pYh;Rp9C1)4?4%EKS!&__eB;ic;8CCsL^j{;ldl{Wz$W zjByx-Nd>@FbW1TrYr%!1>PGxZEuxF~lX}r`XdT~SPPZSL0~e@a=o#^D;!Kw!jl9&U z3cBvqNKX8P>C`*OHN7gyMS|qPKFtO64bN%XD61dmHEyNI?*T4r&fRo}mIh1iaM>2# zXbe|1;2uCbI?!?Rk-5l5o%qqz@Y(Eo5POn%m?)V8WkaCA1gci5k+1S#k5sk51%G8v zkS@3Tn_^b9YSqg0pjd-)war8FH$s?j&BV%dpqNFq<_wU0oky`|rE%Rv%L5CN*-V(f zp^Z|Ue0|9&LodUQ-|BqGmEYB!MD04-Rt18B=U6RDBH1#I2o8)P2@zQTeHKr#%j_6H z3yjKzWW)Px13qJD5j+1K;ox{Tzzj9|zr+YNs>ELnPQ1)I+|)1{MxCUbYzJp-kmv!> zfCwYV1zrl@HTXC$Q_-fg>FI1PIX#||9C)9rKPo?>qRgbTxr%BwnYoh6dKI;AsTgun za(rqmm%2QYN#;^n$xOCpU`KxyR1#hKL7{3>sD20Ct@3SKnM_sj}TzY$2MRburXlOmVbN}gD`S1$WIT)W|Kgj zv`aQK+q!pWnq=C9bUIUe)?3rT?K+*bP3lQE-E?*(!Bt2p~-KYM^`K0Pk&`5YqYKZi`l!N(KIRIQNq4l4WS z8S*_=C~VuRmZX|%m0