Skip to content

Commit

Permalink
Merge branch 'master' into doc-note-edit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ashesh3 authored Jul 19, 2023
2 parents aa780a5 + 9f84bd4 commit bcf1ffa
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 26 deletions.
10 changes: 10 additions & 0 deletions care/facility/api/serializers/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from care.facility.api.serializers.facility import FacilityBareMinimumSerializer
from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
Expand Down Expand Up @@ -165,6 +166,15 @@ class Meta:
exclude = ("deleted", "external_id")


class AssetAvailabilitySerializer(ModelSerializer):
id = UUIDField(source="external_id", read_only=True)
asset = AssetBareMinimumSerializer(read_only=True)

class Meta:
model = AssetAvailabilityRecord
exclude = ("deleted", "external_id")


class UserDefaultAssetLocationSerializer(ModelSerializer):
location_object = AssetLocationSerializer(source="location", read_only=True)

Expand Down
13 changes: 13 additions & 0 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from rest_framework.viewsets import GenericViewSet

from care.facility.api.serializers.asset import (
AssetAvailabilitySerializer,
AssetLocationSerializer,
AssetSerializer,
AssetTransactionSerializer,
Expand All @@ -32,6 +33,7 @@
)
from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
Expand Down Expand Up @@ -128,6 +130,17 @@ def retrieve(self, request, *args, **kwargs):
return Response(hit)


class AssetAvailabilityFilter(filters.FilterSet):
external_id = filters.CharFilter(field_name="asset__external_id")


class AssetAvailabilityViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
queryset = AssetAvailabilityRecord.objects.all().select_related("asset")
serializer_class = AssetAvailabilitySerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = AssetAvailabilityFilter


class AssetViewSet(
ListModelMixin,
RetrieveModelMixin,
Expand Down
66 changes: 66 additions & 0 deletions care/facility/migrations/0372_assetavailabilityrecord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 4.2.2 on 2023-07-18 05:00

import uuid

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("facility", "0371_metaicd11diagnosis_chapter_and_more"),
]

operations = [
migrations.CreateModel(
name="AssetAvailabilityRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"external_id",
models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
),
(
"created_date",
models.DateTimeField(auto_now_add=True, db_index=True, null=True),
),
(
"modified_date",
models.DateTimeField(auto_now=True, db_index=True, null=True),
),
("deleted", models.BooleanField(db_index=True, default=False)),
(
"status",
models.CharField(
choices=[
("Not Monitored", "Not Monitored"),
("Operational", "Operational"),
("Down", "Down"),
("Under Maintenance", "Under Maintenance"),
],
default="Not Monitored",
max_length=20,
),
),
("timestamp", models.DateTimeField()),
(
"asset",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="facility.asset"
),
),
],
options={
"ordering": ["-timestamp"],
"unique_together": {("asset", "timestamp")},
},
),
]
35 changes: 35 additions & 0 deletions care/facility/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ def get_random_asset_id():
return str(uuid.uuid4())


class AvailabilityStatus(models.TextChoices):
NOT_MONITORED = "Not Monitored"
OPERATIONAL = "Operational"
DOWN = "Down"
UNDER_MAINTENANCE = "Under Maintenance"


class AssetLocation(BaseModel, AssetsPermissionMixin):
"""
This model is also used to store rooms that the assets are in, Since these rooms are mapped to
Expand Down Expand Up @@ -105,6 +112,34 @@ def __str__(self):
return self.name


class AssetAvailabilityRecord(BaseModel):
"""
Model to store the availability status of an asset at a particular timestamp.
Fields:
- asset: ForeignKey to Asset model
- status: CharField with choices from AvailabilityStatus
- timestamp: DateTimeField to store the timestamp of the availability record
Note: A pair of asset and timestamp together should be unique, not just the timestamp alone.
"""

asset = models.ForeignKey(Asset, on_delete=models.PROTECT, null=False, blank=False)
status = models.CharField(
choices=AvailabilityStatus.choices,
default=AvailabilityStatus.NOT_MONITORED,
max_length=20,
)
timestamp = models.DateTimeField(null=False, blank=False)

class Meta:
unique_together = (("asset", "timestamp"),)
ordering = ["-timestamp"]

def __str__(self):
return f"{self.asset.name} - {self.status} - {self.timestamp}"


class UserDefaultAssetLocation(BaseModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False)
location = models.ForeignKey(
Expand Down
14 changes: 10 additions & 4 deletions care/facility/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from celery import current_app
from celery.schedules import crontab

from care.facility.tasks.asset_monitor import check_asset_status
from care.facility.tasks.cleanup import delete_old_notifications
from care.facility.tasks.summarisation import (
summarise_district_patient,
Expand All @@ -19,12 +20,12 @@ def setup_periodic_tasks(sender, **kwargs):
name="delete_old_notifications",
)
sender.add_periodic_task(
crontab(hour="*/4", minute=59),
crontab(hour="*/4", minute="59"),
summarise_triage.s(),
name="summarise_triage",
)
sender.add_periodic_task(
crontab(hour=23, minute=59),
crontab(hour="23", minute="59"),
summarise_tests.s(),
name="summarise_tests",
)
Expand All @@ -34,12 +35,17 @@ def setup_periodic_tasks(sender, **kwargs):
name="summarise_facility_capacity",
)
sender.add_periodic_task(
crontab(hour="*/1", minute=59),
crontab(hour="*/1", minute="59"),
summarise_patient.s(),
name="summarise_patient",
)
sender.add_periodic_task(
crontab(hour="*/1", minute=59),
crontab(hour="*/1", minute="59"),
summarise_district_patient.s(),
name="summarise_district_patient",
)
sender.add_periodic_task(
crontab(minute="*/30"),
check_asset_status.s(),
name="check_asset_status",
)
96 changes: 96 additions & 0 deletions care/facility/tasks/asset_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import logging
from datetime import datetime
from typing import Any

from celery import shared_task
from django.utils import timezone

from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AvailabilityStatus,
)
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.base import BaseAssetIntegration

logger = logging.getLogger(__name__)


@shared_task
def check_asset_status():
logger.info(f"Checking Asset Status: {timezone.now()}")

assets = Asset.objects.all()
middleware_status_cache = {}

for asset in assets:
if not asset.asset_class or not asset.meta.get("local_ip_address", None):
continue
try:
hostname = asset.meta.get(
"middleware_hostname",
asset.current_location.facility.middleware_address,
)
result: Any = {}

if hostname in middleware_status_cache:
result = middleware_status_cache[hostname]
else:
try:
asset_class: BaseAssetIntegration = AssetClasses[
asset.asset_class
].value(
{
**asset.meta,
"middleware_hostname": hostname,
}
)
result = asset_class.api_get(asset_class.get_url("devices/status"))
middleware_status_cache[hostname] = result
except Exception:
logger.exception("Error in Asset Status Check - Fetching Status")
middleware_status_cache[hostname] = None
continue

if not result:
continue

new_status = None
for status_record in result:
if asset.meta.get("local_ip_address") in status_record.get(
"status", {}
):
new_status = status_record["status"][
asset.meta.get("local_ip_address")
]
else:
new_status = "not_monitored"

last_record = (
AssetAvailabilityRecord.objects.filter(asset=asset)
.order_by("-timestamp")
.first()
)

if new_status == "up":
new_status = AvailabilityStatus.OPERATIONAL
elif new_status == "down":
new_status = AvailabilityStatus.DOWN
elif new_status == "maintenance":
new_status = AvailabilityStatus.UNDER_MAINTENANCE
else:
new_status = AvailabilityStatus.NOT_MONITORED

if not last_record or (
datetime.fromisoformat(status_record.get("time"))
> last_record.timestamp
and last_record.status != new_status.value
):
AssetAvailabilityRecord.objects.create(
asset=asset,
status=new_status.value,
timestamp=status_record.get("time", timezone.now()),
)

except Exception:
logger.exception("Error in Asset Status Check")
20 changes: 11 additions & 9 deletions care/facility/tests/test_asset_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
class AssetViewSetTestCase(TestBase, TestClassMixin, APITestCase):
asset_id = None

def setUp(self):
self.factory = APIRequestFactory()
state = self.create_state()
district = self.create_district(state=state)
self.user = self.create_user(district=district, username="test user")
facility = self.create_facility(district=district, user=self.user)
self.asset1_location = AssetLocation.objects.create(
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = APIRequestFactory()
state = cls.create_state()
district = cls.create_district(state=state)
cls.user = cls.create_user(district=district, username="test user")
facility = cls.create_facility(district=district, user=cls.user)
cls.asset1_location = AssetLocation.objects.create(
name="asset1 location", location_type=1, facility=facility
)
self.asset = Asset.objects.create(
name="Test Asset", current_location=self.asset1_location, asset_type=50
cls.asset = Asset.objects.create(
name="Test Asset", current_location=cls.asset1_location, asset_type=50
)

def test_list_assets(self):
Expand Down
Loading

0 comments on commit bcf1ffa

Please sign in to comment.