packaged with rye for export_mode
This commit is contained in:
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11.9
|
||||
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[project]
|
||||
name = "room-control"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
authors = [
|
||||
{ name = "John Lancaster", email = "32917998+jsl12@users.noreply.github.com" }
|
||||
]
|
||||
dependencies = [
|
||||
"appdaemon>=4.4.2",
|
||||
"rich>=13.7.1",
|
||||
"pydantic>=2.7.1",
|
||||
"ruff>=0.4.2",
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.8,<3.12"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = 'single'
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = []
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/room_control"]
|
||||
115
requirements-dev.lock
Normal file
115
requirements-dev.lock
Normal file
@@ -0,0 +1,115 @@
|
||||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
# with-sources: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.8.6
|
||||
# via aiohttp-jinja2
|
||||
# via appdaemon
|
||||
# via sockjs
|
||||
aiohttp-jinja2==1.5.1
|
||||
# via appdaemon
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
appdaemon==4.4.2
|
||||
# via room-control
|
||||
astral==3.2
|
||||
# via appdaemon
|
||||
async-timeout==4.0.3
|
||||
# via aiohttp
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
bcrypt==4.0.1
|
||||
# via appdaemon
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
charset-normalizer==3.3.2
|
||||
# via aiohttp
|
||||
# via requests
|
||||
deepdiff==6.3.0
|
||||
# via appdaemon
|
||||
feedparser==6.0.11
|
||||
# via appdaemon
|
||||
frozenlist==1.4.1
|
||||
# via aiohttp
|
||||
# via aiosignal
|
||||
h11==0.14.0
|
||||
# via wsproto
|
||||
idna==3.7
|
||||
# via requests
|
||||
# via yarl
|
||||
iso8601==1.1.0
|
||||
# via appdaemon
|
||||
jinja2==3.1.3
|
||||
# via aiohttp-jinja2
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
paho-mqtt==1.6.1
|
||||
# via appdaemon
|
||||
pid==3.0.4
|
||||
# via appdaemon
|
||||
pydantic==2.7.1
|
||||
# via room-control
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pygments==2.17.2
|
||||
# via rich
|
||||
python-dateutil==2.8.2
|
||||
# via appdaemon
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
python-socketio==5.8.0
|
||||
# via appdaemon
|
||||
pytz==2023.3.post1
|
||||
# via appdaemon
|
||||
pyyaml==6.0.1
|
||||
# via appdaemon
|
||||
requests==2.28.2
|
||||
# via appdaemon
|
||||
rich==13.7.1
|
||||
# via room-control
|
||||
ruff==0.4.2
|
||||
# via room-control
|
||||
sgmllib3k==1.0.0
|
||||
# via feedparser
|
||||
simple-websocket==1.0.0
|
||||
# via python-engineio
|
||||
six==1.16.0
|
||||
# via python-dateutil
|
||||
sockjs==0.11.0
|
||||
# via appdaemon
|
||||
tomli==2.0.1
|
||||
# via appdaemon
|
||||
tomli-w==1.0.0
|
||||
# via appdaemon
|
||||
typing-extensions==4.11.0
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
urllib3==1.26.18
|
||||
# via requests
|
||||
uvloop==0.17.0
|
||||
# via appdaemon
|
||||
websocket-client==1.5.3
|
||||
# via appdaemon
|
||||
wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
115
requirements.lock
Normal file
115
requirements.lock
Normal file
@@ -0,0 +1,115 @@
|
||||
# generated by rye
|
||||
# use `rye lock` or `rye sync` to update this lockfile
|
||||
#
|
||||
# last locked with the following flags:
|
||||
# pre: false
|
||||
# features: []
|
||||
# all-features: false
|
||||
# with-sources: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.8.6
|
||||
# via aiohttp-jinja2
|
||||
# via appdaemon
|
||||
# via sockjs
|
||||
aiohttp-jinja2==1.5.1
|
||||
# via appdaemon
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
appdaemon==4.4.2
|
||||
# via room-control
|
||||
astral==3.2
|
||||
# via appdaemon
|
||||
async-timeout==4.0.3
|
||||
# via aiohttp
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
bcrypt==4.0.1
|
||||
# via appdaemon
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
certifi==2024.2.2
|
||||
# via requests
|
||||
charset-normalizer==3.3.2
|
||||
# via aiohttp
|
||||
# via requests
|
||||
deepdiff==6.3.0
|
||||
# via appdaemon
|
||||
feedparser==6.0.11
|
||||
# via appdaemon
|
||||
frozenlist==1.4.1
|
||||
# via aiohttp
|
||||
# via aiosignal
|
||||
h11==0.14.0
|
||||
# via wsproto
|
||||
idna==3.7
|
||||
# via requests
|
||||
# via yarl
|
||||
iso8601==1.1.0
|
||||
# via appdaemon
|
||||
jinja2==3.1.3
|
||||
# via aiohttp-jinja2
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.0.5
|
||||
# via aiohttp
|
||||
# via yarl
|
||||
ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
paho-mqtt==1.6.1
|
||||
# via appdaemon
|
||||
pid==3.0.4
|
||||
# via appdaemon
|
||||
pydantic==2.7.1
|
||||
# via room-control
|
||||
pydantic-core==2.18.2
|
||||
# via pydantic
|
||||
pygments==2.17.2
|
||||
# via rich
|
||||
python-dateutil==2.8.2
|
||||
# via appdaemon
|
||||
python-engineio==4.9.0
|
||||
# via python-socketio
|
||||
python-socketio==5.8.0
|
||||
# via appdaemon
|
||||
pytz==2023.3.post1
|
||||
# via appdaemon
|
||||
pyyaml==6.0.1
|
||||
# via appdaemon
|
||||
requests==2.28.2
|
||||
# via appdaemon
|
||||
rich==13.7.1
|
||||
# via room-control
|
||||
ruff==0.4.2
|
||||
# via room-control
|
||||
sgmllib3k==1.0.0
|
||||
# via feedparser
|
||||
simple-websocket==1.0.0
|
||||
# via python-engineio
|
||||
six==1.16.0
|
||||
# via python-dateutil
|
||||
sockjs==0.11.0
|
||||
# via appdaemon
|
||||
tomli==2.0.1
|
||||
# via appdaemon
|
||||
tomli-w==1.0.0
|
||||
# via appdaemon
|
||||
typing-extensions==4.11.0
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
urllib3==1.26.18
|
||||
# via requests
|
||||
uvloop==0.17.0
|
||||
# via appdaemon
|
||||
websocket-client==1.5.3
|
||||
# via appdaemon
|
||||
wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
@@ -38,7 +38,11 @@ class Button(Mqtt):
|
||||
topic = f'zigbee2mqtt/{name}'
|
||||
# self.mqtt_subscribe(topic, namespace='mqtt')
|
||||
self.listen_event(
|
||||
self.handle_button, 'MQTT_MESSAGE', topic=topic, namespace='mqtt', button=name
|
||||
self.handle_button,
|
||||
'MQTT_MESSAGE',
|
||||
topic=topic,
|
||||
namespace='mqtt',
|
||||
button=name,
|
||||
)
|
||||
self.log(f'MQTT topic [topic]{topic}[/] controls app [room]{self.app.name}[/]')
|
||||
|
||||
@@ -108,7 +108,11 @@ def room_logging_config(name: str):
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'rich_room': {'formatter': 'rich_room', 'filters': ['room'], **RICH_HANDLER_CFG},
|
||||
'rich_room': {
|
||||
'formatter': 'rich_room',
|
||||
'filters': ['room'],
|
||||
**RICH_HANDLER_CFG,
|
||||
},
|
||||
'file': {
|
||||
'filters': ['unmarkup'],
|
||||
'formatter': 'file',
|
||||
@@ -13,5 +13,9 @@ class Door(Hass):
|
||||
self.app: RoomController = await self.get_app(self.args['app'])
|
||||
self.log(f'Connected to AD app [room]{self.app.name}[/]', level='DEBUG')
|
||||
|
||||
await self.listen_state(self.app.activate_all_off, entity_id=self.args['door'], new='on', cause='door open')
|
||||
|
||||
await self.listen_state(
|
||||
self.app.activate_all_off,
|
||||
entity_id=self.args['door'],
|
||||
new='on',
|
||||
cause='door open',
|
||||
)
|
||||
@@ -47,7 +47,9 @@ class ApplyKwargs(BaseModel):
|
||||
class ControllerStateConfig(BaseModel):
|
||||
time: Optional[str | datetime] = None
|
||||
elevation: Optional[float] = None
|
||||
direction: Optional[Annotated[SunDirection, BeforeValidator(str_to_direction)]] = None
|
||||
direction: Optional[Annotated[SunDirection, BeforeValidator(str_to_direction)]] = (
|
||||
None
|
||||
)
|
||||
off_duration: Optional[OffDuration] = None
|
||||
scene: dict[str, State] | str
|
||||
|
||||
@@ -55,9 +57,13 @@ class ControllerStateConfig(BaseModel):
|
||||
def check_args(cls, values):
|
||||
time, elevation = values.get('time'), values.get('elevation')
|
||||
if time is not None and elevation is not None:
|
||||
raise PydanticCustomError('bad_time_spec', 'Only one of time or elevation can be set.')
|
||||
raise PydanticCustomError(
|
||||
'bad_time_spec', 'Only one of time or elevation can be set.'
|
||||
)
|
||||
elif elevation is not None and 'direction' not in values:
|
||||
raise PydanticCustomError('no_sun_dir', 'Needs sun direction with elevation')
|
||||
raise PydanticCustomError(
|
||||
'no_sun_dir', 'Needs sun direction with elevation'
|
||||
)
|
||||
return values
|
||||
|
||||
def to_apply_kwargs(self, **kwargs):
|
||||
@@ -79,7 +85,9 @@ class RoomControllerConfig(BaseModel):
|
||||
if app_cfg['class'] == 'RoomController':
|
||||
return cls.model_validate(app_cfg)
|
||||
|
||||
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
table = Table(
|
||||
Column('Time', width=15),
|
||||
Column('Scene'),
|
||||
@@ -65,11 +65,14 @@ class Motion(Hass):
|
||||
|
||||
if self.state_mismatch:
|
||||
self.log(
|
||||
f'Sensor is {self.sensor_state} ' f'and light is {self.ref_entity_state}',
|
||||
f'Sensor is {self.sensor_state} '
|
||||
f'and light is {self.ref_entity_state}',
|
||||
level='WARNING',
|
||||
)
|
||||
if self.sensor_state:
|
||||
self.app.activate(kwargs={'cause': f'Syncing state with {self.sensor.entity_id}'})
|
||||
self.app.activate(
|
||||
kwargs={'cause': f'Syncing state with {self.sensor.entity_id}'}
|
||||
)
|
||||
|
||||
# don't need to await these because they'll already get turned into a task by the utils.sync_wrapper decorator
|
||||
self.listen_state(
|
||||
@@ -81,7 +84,9 @@ class Motion(Hass):
|
||||
|
||||
if callbacks := self.callbacks():
|
||||
for handle, entry in callbacks.items():
|
||||
self.log(f'Handle [yellow]{handle[:4]}[/]: {entry.function}', level='DEBUG')
|
||||
self.log(
|
||||
f'Handle [yellow]{handle[:4]}[/]: {entry.function}', level='DEBUG'
|
||||
)
|
||||
|
||||
def callbacks(self):
|
||||
"""Returns a dictionary of validated CallbackEntry objects that are associated with this app"""
|
||||
@@ -98,7 +103,9 @@ class Motion(Hass):
|
||||
oneshot=True,
|
||||
cause='motion on',
|
||||
)
|
||||
self.log(f'Waiting for sensor motion on [friendly_name]{self.sensor.friendly_name}[/]')
|
||||
self.log(
|
||||
f'Waiting for sensor motion on [friendly_name]{self.sensor.friendly_name}[/]'
|
||||
)
|
||||
if self.sensor_state:
|
||||
self.log(
|
||||
f'Sensor [friendly_name]{self.sensor.friendly_name}[/] is already on',
|
||||
@@ -126,14 +133,18 @@ class Motion(Hass):
|
||||
level='WARNING',
|
||||
)
|
||||
|
||||
def callback_light_on(self, entity=None, attribute=None, old=None, new=None, kwargs=None):
|
||||
def callback_light_on(
|
||||
self, entity=None, attribute=None, old=None, new=None, kwargs=None
|
||||
):
|
||||
"""Called when the light turns on"""
|
||||
if new is not None:
|
||||
self.log(f'Detected {entity} turning on', level='DEBUG')
|
||||
duration = self.app.off_duration()
|
||||
self.listen_motion_off(duration)
|
||||
|
||||
def callback_light_off(self, entity=None, attribute=None, old=None, new=None, kwargs=None):
|
||||
def callback_light_off(
|
||||
self, entity=None, attribute=None, old=None, new=None, kwargs=None
|
||||
):
|
||||
"""Called when the light turns off"""
|
||||
self.log(f'Detected {entity} turning off', level='DEBUG')
|
||||
self.listen_motion_on()
|
||||
@@ -165,4 +176,7 @@ class Motion(Hass):
|
||||
if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None:
|
||||
new = m.group('new')
|
||||
self.cancel_listen_state(handle)
|
||||
self.log(f'cancelled callback for sensor {entity} turning {new}', level='DEBUG')
|
||||
self.log(
|
||||
f'cancelled callback for sensor {entity} turning {new}',
|
||||
level='DEBUG',
|
||||
)
|
||||
@@ -79,7 +79,10 @@ class RoomController(Hass, Mqtt):
|
||||
"""
|
||||
# re-parse the state strings into times for the current day
|
||||
self._room_config = RoomControllerConfig.model_validate(self.args)
|
||||
self.log(f'{len(self._room_config.states)} states in the app configuration', level='DEBUG')
|
||||
self.log(
|
||||
f'{len(self._room_config.states)} states in the app configuration',
|
||||
level='DEBUG',
|
||||
)
|
||||
|
||||
for state in self._room_config.states:
|
||||
if state.time is None and state.elevation is not None:
|
||||
Reference in New Issue
Block a user