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}'
|
topic = f'zigbee2mqtt/{name}'
|
||||||
# self.mqtt_subscribe(topic, namespace='mqtt')
|
# self.mqtt_subscribe(topic, namespace='mqtt')
|
||||||
self.listen_event(
|
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}[/]')
|
self.log(f'MQTT topic [topic]{topic}[/] controls app [room]{self.app.name}[/]')
|
||||||
|
|
||||||
@@ -108,7 +108,11 @@ def room_logging_config(name: str):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'handlers': {
|
'handlers': {
|
||||||
'rich_room': {'formatter': 'rich_room', 'filters': ['room'], **RICH_HANDLER_CFG},
|
'rich_room': {
|
||||||
|
'formatter': 'rich_room',
|
||||||
|
'filters': ['room'],
|
||||||
|
**RICH_HANDLER_CFG,
|
||||||
|
},
|
||||||
'file': {
|
'file': {
|
||||||
'filters': ['unmarkup'],
|
'filters': ['unmarkup'],
|
||||||
'formatter': 'file',
|
'formatter': 'file',
|
||||||
@@ -13,5 +13,9 @@ class Door(Hass):
|
|||||||
self.app: RoomController = await self.get_app(self.args['app'])
|
self.app: RoomController = await self.get_app(self.args['app'])
|
||||||
self.log(f'Connected to AD app [room]{self.app.name}[/]', level='DEBUG')
|
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):
|
class ControllerStateConfig(BaseModel):
|
||||||
time: Optional[str | datetime] = None
|
time: Optional[str | datetime] = None
|
||||||
elevation: Optional[float] = 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
|
off_duration: Optional[OffDuration] = None
|
||||||
scene: dict[str, State] | str
|
scene: dict[str, State] | str
|
||||||
|
|
||||||
@@ -55,9 +57,13 @@ class ControllerStateConfig(BaseModel):
|
|||||||
def check_args(cls, values):
|
def check_args(cls, values):
|
||||||
time, elevation = values.get('time'), values.get('elevation')
|
time, elevation = values.get('time'), values.get('elevation')
|
||||||
if time is not None and elevation is not None:
|
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:
|
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
|
return values
|
||||||
|
|
||||||
def to_apply_kwargs(self, **kwargs):
|
def to_apply_kwargs(self, **kwargs):
|
||||||
@@ -79,7 +85,9 @@ class RoomControllerConfig(BaseModel):
|
|||||||
if app_cfg['class'] == 'RoomController':
|
if app_cfg['class'] == 'RoomController':
|
||||||
return cls.model_validate(app_cfg)
|
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(
|
table = Table(
|
||||||
Column('Time', width=15),
|
Column('Time', width=15),
|
||||||
Column('Scene'),
|
Column('Scene'),
|
||||||
@@ -65,11 +65,14 @@ class Motion(Hass):
|
|||||||
|
|
||||||
if self.state_mismatch:
|
if self.state_mismatch:
|
||||||
self.log(
|
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',
|
level='WARNING',
|
||||||
)
|
)
|
||||||
if self.sensor_state:
|
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
|
# don't need to await these because they'll already get turned into a task by the utils.sync_wrapper decorator
|
||||||
self.listen_state(
|
self.listen_state(
|
||||||
@@ -81,7 +84,9 @@ class Motion(Hass):
|
|||||||
|
|
||||||
if callbacks := self.callbacks():
|
if callbacks := self.callbacks():
|
||||||
for handle, entry in callbacks.items():
|
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):
|
def callbacks(self):
|
||||||
"""Returns a dictionary of validated CallbackEntry objects that are associated with this app"""
|
"""Returns a dictionary of validated CallbackEntry objects that are associated with this app"""
|
||||||
@@ -98,7 +103,9 @@ class Motion(Hass):
|
|||||||
oneshot=True,
|
oneshot=True,
|
||||||
cause='motion on',
|
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:
|
if self.sensor_state:
|
||||||
self.log(
|
self.log(
|
||||||
f'Sensor [friendly_name]{self.sensor.friendly_name}[/] is already on',
|
f'Sensor [friendly_name]{self.sensor.friendly_name}[/] is already on',
|
||||||
@@ -126,14 +133,18 @@ class Motion(Hass):
|
|||||||
level='WARNING',
|
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"""
|
"""Called when the light turns on"""
|
||||||
if new is not None:
|
if new is not None:
|
||||||
self.log(f'Detected {entity} turning on', level='DEBUG')
|
self.log(f'Detected {entity} turning on', level='DEBUG')
|
||||||
duration = self.app.off_duration()
|
duration = self.app.off_duration()
|
||||||
self.listen_motion_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"""
|
"""Called when the light turns off"""
|
||||||
self.log(f'Detected {entity} turning off', level='DEBUG')
|
self.log(f'Detected {entity} turning off', level='DEBUG')
|
||||||
self.listen_motion_on()
|
self.listen_motion_on()
|
||||||
@@ -165,4 +176,7 @@ class Motion(Hass):
|
|||||||
if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None:
|
if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None:
|
||||||
new = m.group('new')
|
new = m.group('new')
|
||||||
self.cancel_listen_state(handle)
|
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
|
# re-parse the state strings into times for the current day
|
||||||
self._room_config = RoomControllerConfig.model_validate(self.args)
|
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:
|
for state in self._room_config.states:
|
||||||
if state.time is None and state.elevation is not None:
|
if state.time is None and state.elevation is not None:
|
||||||
Reference in New Issue
Block a user