def revise_plants_status(current_pot: Pot, new_plants_status: dict): revised_plants_status = {} new_plants_objs: Plants = Plants.parse_obj(new_plants_status) old_plants_objs: Plants = current_pot.session.plants # Fields are ordered https://pydantic-docs.helpmanual.io/usage/models/#field-ordering for old_plant_tuple, new_plant_tuple in zip(old_plants_objs, new_plants_objs): old_plant: Plant = old_plant_tuple[1] new_plant: Plant = new_plant_tuple[1] ring_colour = old_plant.ringColour # NOTE: After harvest, slot will be empty so None. For UT, no new seeds after harvest, so keep it at None # TODO: Ideally to remove this once UI allows users to indicate to plant new seed if old_plant.growthStage == None: new_plant = Plant(growthStage=None, ringColour=new_plant.ringColour) # TODO: Future work: start time of seed planting based on user indication in app, not session start time # NOTE: Add replace(tzinfo=None) to avoid error "can't subtract offset-naive and offset-aware datetimes" elif is_seed(datetime.utcnow(), current_pot.session.sessionStartTime.replace(tzinfo=None)): new_plant.growthStage = GrowthStage.seed elif is_sprouting(new_plant.growthStage, datetime.utcnow(), current_pot.session.sessionStartTime.replace(tzinfo=None)): new_plant.growthStage = GrowthStage.sprouting else: logger.info("No revision to plants status needed") revised_plants_status[ring_colour] = new_plant.dict() return revised_plants_status
async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections[websocket.path_params['pot_id']] = websocket logger.info("WS connected with Pot {}".format( websocket.path_params['pot_id'])) logger.info("Connected WSs - {}".format( self.active_connections.keys()))
async def pots_health_check(): all_pots = pots_collection.get() for pot in all_pots: try: pot_id = pot.to_dict()['potId'] # Alert Pot health_check_msg = MessageToPot( action=Action.update, potId=pot_id, data=[ PotSendDataDictStr(field=PotSendDataStr.health_check, value="health check") ]) await ws_manager.send_personal_message_json( health_check_msg.dict(), pot_id) logger.info("Health check to pot {} success!".format(pot_id)) firestore_input = {"connected": True} except Exception as e: logger.error("Health check to Pot {} failed!".format(pot_id)) firestore_input = {"connected": False} # Update Firebase to alert mobile app pots_collection.document(pot_id).update(firestore_input)
async def create(new_pot: PotHttpReq): try: pot_id = new_pot.id new_pot = new_pot_registration(pot_id) pots_collection.document(pot_id).set(new_pot.dict()) logger.info("New Pot {} registered".format(pot_id)) return {"success": True} except Exception as e: logger.error(e) return f"An Error Occured: {e}"
async def send_personal_message_json(self, message: dict, pot_id: str): if pot_id in self.active_connections: websocket: WebSocket = self.active_connections[pot_id] await websocket.send_json(message) logger.info(message) else: message["error_msg"] = "Websocket for Pot {} not found".format( pot_id) logger.error(message) raise Exception("Websocket for Pot {} not found".format(pot_id))
async def broadcast(self, message_dict: be2pot_schemas.PotSendDataDictStr): if len(self.active_connections) > 0: for pot_id in self.active_connections: websocket: WebSocket = self.active_connections[pot_id] health_check_msg = be2pot_schemas.MessageToPot( action=be2pot_schemas.Action.read, potId=pot_id, data=[ be2pot_schemas.PotSendDataDictStr( field=message_dict.field, value=message_dict.value) ]) await websocket.send_json(health_check_msg.dict()) logger.info(message_dict) else: logger.warning("No websocket connections to broadcast to")
async def websocket_endpoint(websocket: WebSocket, pot_id: str): await ws_manager.connect(websocket) try: while True: data = await websocket.receive_json() logger.info(data) responses = await ws_manager.process_message(data) for response in responses: await ws_manager.send_personal_message_json( response.dict(), pot_id) # await manager.broadcast(f"Client #{pot_id} says: {data}") # TODO: Maybe can remove this except pydantic.error_wrappers.ValidationError as e: logger.error(e) exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] err_response = be2pot_schemas.MessageToPot( potId=pot_id, data=[ be2pot_schemas.PotSendDataDictStr( field=be2pot_schemas.PotSendDataStr.error, value="{}: {}, line {}, {}".format(exc_type, fname, exc_tb.tb_lineno, e)) ]) await ws_manager.send_personal_message_json(err_response.dict(), pot_id) except WebSocketDisconnect: ws_manager.disconnect(pot_id) except Exception as e: logger.error(e) exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] err_response = be2pot_schemas.MessageToPot( potId=pot_id, data=[ be2pot_schemas.PotSendDataDictStr( field=be2pot_schemas.PotSendDataStr.error, value="{}: {}, line {}, {}".format(exc_type, fname, exc_tb.tb_lineno, e)) ]) await ws_manager.send_personal_message_json(err_response.dict(), pot_id)
async def quiz_alert(): current_date = datetime.utcnow().strftime('%Y%m%d') # NOTE: For Python, all string fields with an integer value like '1' require `` retrieved_pots = pots_collection.where('session.quiz.quizDates', 'array_contains', current_date).get() # TODO: parse this properly to Pot object for pot in retrieved_pots: try: pot_id = pot.to_dict()["potId"] quiz_day_number_idx = pot.to_dict( )['session']['quiz']['quizDates'].index(current_date) quiz_day_number = pot.to_dict( )['session']['quiz']['quizDayNumbers'][quiz_day_number_idx] current_show_quiz_numbers: list = pot.to_dict( )['session']['quiz']['showQuizNumbers'] current_show_quiz_numbers.append(quiz_day_number) firestore_input = { "session.quiz.showQuizNumbers": current_show_quiz_numbers, "session.quiz.currentQuizDayNumber": quiz_day_number } # Update Firebase to alert mobile app pots_collection.document(pot_id).update(firestore_input) logger.info("Updated Quiz {} alert for Pot {} to database".format( quiz_day_number, pot_id)) #TODO: Also alert when previous quiz not yet completed # Alert Pot alert_message = MessageToPot( action=Action.update, potId=pot_id, data=[ PotSendDataDictBool(field=PotSendDataBool.showQuiz, value=True) ]) await ws_manager.send_personal_message_json( alert_message.dict(), pot_id) logger.info("Sent Quiz {} alert to Pot {}".format( quiz_day_number, pot_id)) except Exception as e: logger.error("Quiz {} alert to Pot {} failed!".format( quiz_day_number, pot_id))
async def harvest(pot: PotHttpReq): try: pot_id = pot.id response = MessageToPot(action=Action.read, potId=pot_id, data=[ PotSendDataDictStr( field=PotSendDataStr.image, value="send image over") ]) await ws_manager.send_personal_message_json(response.dict(), pot_id) logger.info("CV read message sent to pot {}".format(pot_id)) return {"success": True} except Exception as e: logger.error(e) return {"success": False}
async def health(pot: PotHttpReq): try: pot_id = pot.id health_check_msg = MessageToPot( potId=pot_id, data=[ PotSendDataDictStr(field=PotSendDataStr.health_check, value="health check") ]) await ws_manager.send_personal_message_json(health_check_msg.dict(), pot_id) logger.info("Health check to pot {} success!".format(pot_id)) firestore_input = {"connected": True} pots_collection.document(pot_id).update(firestore_input) return {"health check": True} except Exception as e: logger.error("Health check to pot {} failed!".format(pot_id)) return {"health check": False}
async def daily_check_in_alert(): all_pots = pots_collection.get() for pot in all_pots: try: pot_id = pot.to_dict()['potId'] firestore_input = {"session.checkIn.showCheckIn": True} # Update Firebase to alert mobile app pots_collection.document(pot_id).update(firestore_input) # Alert Pot alert_message = MessageToPot( action=Action.update, potId=pot_id, data=[ PotSendDataDictBool(field=PotSendDataBool.showCheckIn, value=True) ]) await ws_manager.send_personal_message_json( alert_message.dict(), pot_id) logger.info("Sent Check In alert to Pot {}".format(pot_id)) # TODO: Need a message queue for messages not sent to pots with failed websocket connection except Exception as e: logger.error("Check In alert to Pot {} failed!".format(pot_id))
def get_firebase_credentials(): if os.path.exists(FIREBASE_CRED) and os.path.isfile(FIREBASE_CRED): logger.info("Credentials JSON file found") return FIREBASE_CRED else: logger.info("Credentials JSON file not found.") encoded_cred = os.getenv('FIREBASE_CRED_ENCODED') if encoded_cred != None: logger.info("Credentials env var found") decoded_cred = json.loads(base64.b64decode(encoded_cred)) return decoded_cred else: raise Exception("'FIREBASE_CRED_ENCODED' env var undefined.")
from dotenv import load_dotenv load_dotenv() import sys from fastapi import FastAPI import uvicorn from lib.custom_logger import logger from ws import ws_server, firebase_listener from router import pots, plants from scheduler import scheduler from lib import check_in # Initialize FastAPI app app = FastAPI() app.include_router(pots.router) app.include_router(plants.router) app.include_router(ws_server.router) logger.info("Server started") if __name__ == '__main__': sys.exit("Run: `uvicorn main:app --reload --port 8000` instead")
def check_existing_connections(self): logger.info("Existing Connections - {}".format( list(self.active_connections.keys())))