packaged with rye for export_mode

This commit is contained in:
John Lancaster
2024-05-02 22:27:46 -05:00
parent d42afb1829
commit 069e0c0f42
11 changed files with 316 additions and 16 deletions

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.11.9

32
pyproject.toml Normal file
View 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
View 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
View 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

View File

@@ -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}[/]')

View File

@@ -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',

View 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',
)

View File

@@ -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'),

View File

@@ -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',
)

View File

@@ -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: