Skip to content

Commit

Permalink
Fastapi deprecation 1208 (#1276)
Browse files Browse the repository at this point in the history
* FastAPI startup events

This was added a few months ago but seems to have since been clobbered. Testing again.

* Missing global

* Use objects

* Passing the lifespan to the FastAPI constructor

* Make lifespan async

* Tying it all back together.

* Warning for no status instead of error.

* More missing values

* Converting weather station fastapi service.

* Namespace conflict

* Remove old `on_startup`

* Replace repeat_every

* Replace repeat_every with simple thread

* Join the threads.

* Less verbose

* Adding host info for working with remote weather stations on cli
  • Loading branch information
wtgee committed May 24, 2024
1 parent 9a0dadc commit 4ab8d1f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 75 deletions.
67 changes: 41 additions & 26 deletions src/panoptes/pocs/sensor/power.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
from contextlib import suppress
from dataclasses import dataclass
from enum import IntEnum
from typing import Optional, Dict, List, Callable
from functools import partial
import pandas as pd
from panoptes.utils import error

from streamz.dataframe import PeriodicDataFrame
from typing import Optional, Dict, List, Callable

import pandas as pd
from astropy import units as u

from panoptes.utils import error
from panoptes.utils.serial.device import find_serial_port, SerialDevice
from panoptes.utils.serializers import to_json, from_json
from panoptes.pocs.base import PanBase
from panoptes.utils.time import current_time
from streamz.dataframe import PeriodicDataFrame

from panoptes.pocs.base import PanBase


class PinState(IntEnum):
Expand Down Expand Up @@ -106,7 +105,8 @@ def __init__(self,
dataframe_period: int = 1,
mean_interval: Optional[int] = 5,
arduino_board_name: str = 'power_board',
*args, **kwargs):
*args, **kwargs
):
"""Initialize the power board.
The `relays` should be a dictionary with the relay name as key and a
Expand Down Expand Up @@ -146,11 +146,12 @@ def __init__(self,

self.logger.debug(f'Setting up Power board connection for {name=} on {self.port}')
self._ignore_readings = 5
self.arduino_board = SerialDevice(port=self.port,
serial_settings=dict(baudrate=9600),
reader_callback=reader_callback,
name=arduino_board_name
)
self.arduino_board = SerialDevice(
port=self.port,
serial_settings=dict(baudrate=9600),
reader_callback=reader_callback,
name=arduino_board_name
)

self.relays: List[Relay] = list()
self.relay_labels: Dict[str, Relay] = dict()
Expand All @@ -164,8 +165,10 @@ def __init__(self,

self.dataframe = None
if dataframe_period is not None:
self.dataframe = PeriodicDataFrame(interval=f'{dataframe_period}s',
datafn=self.to_dataframe)
self.dataframe = PeriodicDataFrame(
interval=f'{dataframe_period}s',
datafn=self.to_dataframe
)

self._mean_interval = mean_interval

Expand All @@ -174,6 +177,12 @@ def __init__(self,
@property
def status(self):
readings = self.readings
if not readings:
self.logger.warning(
'No readings available. '
'If system just started please wait a moment.'
)
return {}
status = {
r.name: dict(label=r.label, state=r.state.name, reading=readings[r.label])
for r in self.relays
Expand All @@ -188,6 +197,9 @@ def readings(self):
"""Return the rolling mean of the readings. """
time_start = (current_time() - self._mean_interval * u.second).to_datetime()
df = self.to_dataframe()[time_start:]
if len(df) == 0:
return {}

values = df.mean().astype('int').to_dict()

# Add the most recent ac_ok and battery_low check.
Expand Down Expand Up @@ -255,11 +267,12 @@ def setup_relays(self, relays: Dict[str, dict]):

# Create relay object.
self.logger.debug(f'Creating {relay_label=} for {relay_config!r}')
relay = Relay(name=relay_name,
label=relay_config.get('label', ''),
relay_index=relay_index,
default_state=default_state
)
relay = Relay(
name=relay_name,
label=relay_config.get('label', ''),
relay_index=relay_index,
default_state=default_state
)

# Add convenience methods on the relay itself.
setattr(relay, 'turn_on', partial(self.turn_on, relay.label))
Expand Down Expand Up @@ -309,7 +322,7 @@ def default_reader_callback(self, data):

# Check we got a valid reading.
if len(data[relay_key]) != len(TruckerRelayIndex) \
and len(data[values_key]) != len(TruckerRelayIndex):
and len(data[values_key]) != len(TruckerRelayIndex):
self.logger.debug('Did not get a full valid reading')
return

Expand All @@ -336,11 +349,13 @@ def __str__(self):
return f'{self.name} [{relay_states}]'

def __repr__(self):
return to_json({
'name': self.name,
'port': self.port,
'relays': [dict(name=r.name, label=r.label, state=r.state.name) for r in self.relays],
})
return to_json(
{
'name': self.name,
'port': self.port,
'relays': [dict(name=r.name, label=r.label, state=r.state.name) for r in self.relays],
}
)

@classmethod
def lookup_port(cls, vendor_id=0x2341, product_id=0x0043, **kwargs):
Expand Down
30 changes: 26 additions & 4 deletions src/panoptes/pocs/utils/cli/weather.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
import subprocess
from dataclasses import dataclass

import requests
import typer
from rich import print


@dataclass
class HostInfo:
host: str = 'localhost'
port: str = '6566'

@property
def url(self):
return f'http://{self.host}:{self.port}'


app = typer.Typer()


@app.callback()
def common(context: typer.Context,
host: str = typer.Option('localhost', help='Weather station host address.'),
port: str = typer.Option('6566', help='Weather station port.'),
):
context.obj = HostInfo(host=host, port=port)


@app.command(name='status', help='Get the status of the weather station.')
def status(page='status', base_url='http://localhost:6566'):
def status(context: typer.Context, page='status'):
"""Get the status of the weather station."""
print(get_page(page, base_url))
url = context.obj.url
print(get_page(page, url))


@app.command(name='config', help='Get the configuration of the weather station.')
def config(page='config', base_url='http://localhost:6566'):
def config(context: typer.Context, page='config'):
"""Get the configuration of the weather station."""
print(get_page(page, base_url))
url = context.obj.url
print(get_page(page, url))


def get_page(page, base_url):
Expand Down
57 changes: 38 additions & 19 deletions src/panoptes/pocs/utils/service/power.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import time
from contextlib import asynccontextmanager
from enum import auto
from threading import Thread
from typing import Union

from fastapi import FastAPI
from fastapi_utils.enums import StrEnum
from fastapi_utils.tasks import repeat_every
from panoptes.utils.config.client import get_config
from pydantic import BaseModel

Expand All @@ -20,37 +22,54 @@ class RelayCommand(BaseModel):
command: RelayAction


app = FastAPI()
power_board: PowerBoard
conf = get_config('environment.power', {})
app_objects = {}


@app.on_event('startup')
async def startup():
global power_board
power_board = PowerBoard(**get_config('environment.power', {}))
print(f'Power board setup: {power_board}')
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Context manager for the lifespan of the app.
This will connect to the power board and record readings at a regular interval.
"""
conf: dict = get_config('environment.power', {})
power_board = PowerBoard(**conf)
power_board.logger.info(f'Power board setup: {power_board}')
app_objects['power_board'] = power_board
app_objects['conf'] = conf

# Set up a thread to record the readings at an interval.
def record_readings():
"""Record the current readings in the db."""
record_interval = conf.get('record_interval', 60)
power_board.logger.info(f'Setting up power recording {record_interval=}')
while True:
time.sleep(record_interval)
power_board.record(collection_name='power')

# Create a thread to record the readings at an interval.
power_thread = Thread(target=record_readings)
power_thread.daemon = True
power_thread.start()

yield
power_board.logger.info('Shutting down power board, please wait.')
power_thread.join()


@app.on_event('startup')
@repeat_every(seconds=conf.get('record_interval', 60), wait_first=True)
def record_readings():
"""Record the current readings in the db."""
global power_board
return power_board.record(collection_name='power')
app = FastAPI(lifespan=lifespan)


@app.get('/')
async def root():
"""Returns the power board status."""
global power_board
power_board = app_objects['power_board']
return power_board.status


@app.get('/readings')
async def readings():
"""Return the current readings as a dict."""
global power_board
power_board = app_objects['power_board']
return power_board.to_dataframe().to_dict()


Expand All @@ -63,7 +82,7 @@ def control_relay(relay_command: RelayCommand):
@app.get('/relay/{relay}/control/{command}')
def control_relay_url(relay: Union[int, str], command: str = 'turn_on'):
"""Control a relay via a GET request"""
return do_command(RelayCommand(relay=relay, command=command))
return do_command(RelayCommand(relay=relay, command=RelayAction(command)))


def do_command(relay_command: RelayCommand):
Expand All @@ -72,7 +91,7 @@ def do_command(relay_command: RelayCommand):
This function performs the actual relay control and is used by both request
types.
"""
global power_board
power_board = app_objects['power_board']
relay_id = relay_command.relay
try:
relay = power_board.relay_labels[relay_id]
Expand Down
Loading

0 comments on commit 4ab8d1f

Please sign in to comment.