Skip to content

Commit

Permalink
Prevent dome from opening during daytime (#30)
Browse files Browse the repository at this point in the history
* Prevent dome from opening during daytime

* Do not fail PRs if the project coverage is not reached

* Additional tests

* Update changelog

* Monitor and close dome during daytime
  • Loading branch information
albireox authored Dec 25, 2024
1 parent 687e06e commit 3b69593
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 8 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

### 🚀 New

* [#29](https://vscode.dev/github/sdss/lvmecp/pull/29) Add a new engineering mode that can be used to bypass the heartbeat and to allow the dome to open during daytime.
* [#29](https://github.com/sdss/lvmecp/pull/29) Add a new engineering mode that can be used to bypass the heartbeat and to allow the dome to open during daytime.
* [#30](https://github.com/sdss/lvmecp/pull/30) Prevent the dome from opening during daytime. Close if daytime is detected.

### ✨ Improved

Expand All @@ -13,7 +14,7 @@

### 🏷️ Changed

* [#28](https://vscode.dev/github/sdss/lvmecp/pull/28) Removed the automatic setting of the heartbeat variable. Added a `heartbeat` command that will be triggered by a heartbeat middleware.
* [#28](https://github.com/sdss/lvmecp/pull/28) Removed the automatic setting of the heartbeat variable. Added a `heartbeat` command that will be triggered by a heartbeat middleware.

### 🔧 Fixed

Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ coverage:
target: 85%
if_not_found: success
if_ci_failed: error
informational: false
informational: true
only_pulls: true
patch:
default:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies = [
"click-default-group>=1.2.2",
"pyserial-asyncio>=0.6",
"pymodbus>=3.6.0,<3.7",
"lvmopstools>=0.2.0"
"lvmopstools[ephemeris]>=0.5.2",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions python/lvmecp/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async def actor(ctx, with_simulator: bool = False):
schema_rel = ecp_config["actor"]["schema"]
ecp_config["actor"]["schema"] = str(pathlib.Path(__file__).parent / schema_rel)

config.load(ecp_config) # Update internal configuration
actor_obj = ECPActor.from_config(ecp_config)

if ctx.obj["verbose"]:
Expand Down
16 changes: 16 additions & 0 deletions python/lvmecp/actor/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
self.semaphore = asyncio.Semaphore(5)

self._emit_status_task: asyncio.Task | None = None
self._monitor_dome_task: asyncio.Task | None = None

self._engineering_mode: bool = False
self._engineering_mode_task: asyncio.Task | None = None
Expand All @@ -81,13 +82,16 @@ async def start(self, **kwargs):
await self.plc.start_modules()

self._emit_status_task = asyncio.create_task(self.emit_status())
self._monitor_dome_task = asyncio.create_task(self.monitor_dome())

return self

async def stop(self, **kwargs):
"""Stops the actor."""

self._emit_status_task = await cancel_task(self._emit_status_task)
self._monitor_dome_task = await cancel_task(self._monitor_dome_task)

self._engineering_mode_task = await cancel_task(self._engineering_mode_task)

await super().stop(**kwargs)
Expand All @@ -102,6 +106,18 @@ async def emit_status(self, delay: float = 30.0):
await self.send_command(self.name, "status", internal=True)
await asyncio.sleep(delay)

async def monitor_dome(self, delay: float = 30.0):
"""Monitors the dome and closes during daytime."""

while True:
await asyncio.sleep(delay)

if self._engineering_mode:
pass
elif self.plc.dome.is_daytime():
self.write("w", text="Dome found open during daytime. Closing.")
await self.plc.dome.close()

async def engineering_mode(
self,
enable: bool,
Expand Down
52 changes: 51 additions & 1 deletion python/lvmecp/dome.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from time import time
from types import SimpleNamespace

from lvmecp import log
from astropy.time import Time
from lvmopstools.ephemeris import get_ephemeris_summary

from lvmecp import config, log
from lvmecp.exceptions import DomeError, ECPWarning
from lvmecp.maskbits import DomeStatus
from lvmecp.module import PLCModule
Expand Down Expand Up @@ -145,6 +148,9 @@ async def _move(self, open: bool, force: bool = False):
async def open(self, force: bool = False):
"""Open the dome."""

if not self.is_allowed():
raise DomeError("Dome cannot be opened during daytime.")

await self._move(True, force=force)

async def close(self, force: bool = False):
Expand Down Expand Up @@ -172,3 +178,47 @@ async def reset(self):

await self.modbus["rolloff_error_reset"].set(1)
await asyncio.sleep(1)

def is_allowed(self):
"""Returns whether the dome is allowed to move.
Currently the only check performed is to confirm that it is not daytime,
but this method could be expanded in the future.
"""

is_daytime: bool | None
if not config["dome.daytime_allowed"]:
is_daytime = self.is_daytime()
else:
is_daytime = None

if not is_daytime:
return True

if self.plc._actor and self.plc._actor._engineering_mode:
self.plc._actor.write(
"w",
text="Daytime detected but engineering mode is active. "
"Allowing to open the dome.",
)
return True

return False

def is_daytime(self): # pragma: no cover
"""Returns whether it is daytime."""

daytime_tolerance = config["dome.daytime_tolerance"] or 0.0

ephemeris = get_ephemeris_summary()
sunset = ephemeris["sunset"] - daytime_tolerance / 86400
sunrise = ephemeris["sunrise"] + daytime_tolerance / 86400

now = Time.now().jd
assert isinstance(now, float)

if now < sunset or now > sunrise:
return True

return False
4 changes: 4 additions & 0 deletions python/lvmecp/etc/lvmecp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ safety:
override_local_mode: False
o2_threshold: 19.5

dome:
daytime_allowed: false
daytime_tolerance: 600

hvac:
host: 10.8.38.49
port: 502
Expand Down
2 changes: 2 additions & 0 deletions python/lvmecp/plc.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def __init__(
self.config = config
self.modbus = Modbus(config=config["modbus"])

self._actor = actor

self.dome = DomeController(
"dome",
self,
Expand Down
111 changes: 111 additions & 0 deletions tests/test_command_dome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @Author: José Sánchez-Gallego ([email protected])
# @Date: 2024-12-24
# @Filename: test_command_dome.py
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)

from __future__ import annotations

import asyncio

from typing import TYPE_CHECKING

import lvmecp.dome
from lvmecp.maskbits import DomeStatus


if TYPE_CHECKING:
from pytest_mock import MockerFixture

from lvmecp.actor import ECPActor


async def test_command_dome_open(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=False)
mocker.patch.object(actor.plc.dome, "_move", return_value=True)

mocker.patch.object(actor.plc.dome, "status", return_value=DomeStatus.OPEN)

cmd = await actor.invoke_mock_command("dome open")
await cmd

assert cmd.status.did_succeed


async def test_command_dome_close(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "_move", return_value=True)

mocker.patch.object(actor.plc.dome, "status", return_value=DomeStatus.CLOSED)

cmd = await actor.invoke_mock_command("dome close")
await cmd

assert cmd.status.did_succeed


async def test_command_dome_daytime(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=True)
mocker.patch.object(actor.plc.dome, "_move", return_value=True)

cmd = await actor.invoke_mock_command("dome open")
await cmd

assert cmd.status.did_fail


async def test_command_dome_daytime_allowed(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(
lvmecp.dome,
"config",
return_value={"dome": {"daytime_allowed": True}},
)
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=True)
mocker.patch.object(actor.plc.dome, "_move", return_value=True)

mocker.patch.object(actor.plc.dome, "status", return_value=DomeStatus.OPEN)

cmd = await actor.invoke_mock_command("dome open")
await cmd

assert cmd.status.did_succeed


async def test_command_dome_daytime_eng_mode(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=True)
mocker.patch.object(actor.plc.dome, "_move", return_value=True)
mocker.patch.object(actor, "_engineering_mode", return_value=True)

mocker.patch.object(actor.plc.dome, "status", return_value=DomeStatus.OPEN)

cmd = await actor.invoke_mock_command("dome open")
await cmd

assert cmd.status.did_succeed


async def test_actor_daytime_task(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=True)
dome_close_mock = mocker.patch.object(actor.plc.dome, "close")

task = asyncio.create_task(actor.monitor_dome(delay=0.1))
await asyncio.sleep(0.2)

dome_close_mock.assert_called_once()

task.cancel()


async def test_actor_daytime_task_eng_mode(actor: ECPActor, mocker: MockerFixture):
mocker.patch.object(actor.plc.dome, "is_daytime", return_value=True)
mocker.patch.object(actor, "_engineering_mode", return_value=True)

dome_close_mock = mocker.patch.object(actor.plc.dome, "close")

task = asyncio.create_task(actor.monitor_dome(delay=0.1))
await asyncio.sleep(0.2)

dome_close_mock.assert_not_called()

task.cancel()
2 changes: 1 addition & 1 deletion tests/test_command_engineering_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# @Author: José Sánchez-Gallego ([email protected])
# @Date: 2024-12-24
# @Filename: test_command_heartbeat.py
# @Filename: test_command_engineering_mode.py
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)

from __future__ import annotations
Expand Down
Loading

0 comments on commit 3b69593

Please sign in to comment.