diff --git a/neon_skill_speed_test/__init__.py b/neon_skill_speed_test/__init__.py index 766d3e1..025a7a6 100644 --- a/neon_skill_speed_test/__init__.py +++ b/neon_skill_speed_test/__init__.py @@ -32,7 +32,9 @@ from ovos_utils.log import LOG from ovos_utils.process_utils import RuntimeRequirements from neon_utils.skills.neon_skill import NeonSkill -from ovos_workshop.decorators import intent_handler +from ovos_workshop.decorators import intent_handler, skill_api_method + +from neon_skill_speed_test.models import SpeedTestResult class SpeedTestSkill(NeonSkill): @@ -59,6 +61,16 @@ def runtime_requirements(self): no_network_fallback=False, no_gui_fallback=True) + @skill_api_method + def run_speed_test(self) -> SpeedTestResult: + """ + Run a speed test on the system hosting this skill and return the + measured upload, download, and ping results. + """ + self.test.download() + self.test.upload() + return SpeedTestResult(**self.test.results.dict()) + @intent_handler("run_speed_test.intent") def handle_run_speed_test(self, message): self.speak_dialog("start_test") @@ -66,13 +78,11 @@ def handle_run_speed_test(self, message): "ovos.notification.api.set.controlled", {"sender": self.skill_id, "text": self.translate("notify_testing")})) - self.test.download() - self.test.upload() - res = self.test.results.dict() + res = self.run_speed_test() # TODO: Better rounding logic for kbps vs Mbps vs Gbps - down = round(res['download']/1000000) - up = round(res['upload']/1000000) - ping = round(res['ping']) + down = round(res.download/1000000) + up = round(res.upload/1000000) + ping = round(res.ping) LOG.info(res) self.bus.emit(message.forward( "ovos.notification.api.remove.controlled")) diff --git a/neon_skill_speed_test/models.py b/neon_skill_speed_test/models.py new file mode 100644 index 0000000..7dd7a31 --- /dev/null +++ b/neon_skill_speed_test/models.py @@ -0,0 +1,35 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2025 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from pydantic import BaseModel, Field + + +class SpeedTestResult(BaseModel): + download: float = Field(description="Download speed in bits per second") + upload: float = Field(description="Upload speed in bits per second") + ping: float = Field(description="Ping in milliseconds") diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 148cf99..aed8bb0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,4 +2,5 @@ neon-utils~=1.12 speedtest-cli~=2.1 ovos-utils~=0.0, >=0.0.28 ovos-bus-client~=0.0,>=0.0.3 -ovos-workshop~=0.0,>=0.0.15 \ No newline at end of file +ovos-workshop~=0.0,>=0.0.15 +pydantic~=2.0 diff --git a/skill.json b/skill.json index 07cb659..a9b2ab6 100644 --- a/skill.json +++ b/skill.json @@ -17,6 +17,7 @@ "ovos-bus-client~=0.0,>=0.0.3", "ovos-utils~=0.0, >=0.0.28", "ovos-workshop~=0.0,>=0.0.15", + "pydantic~=2.0", "speedtest-cli~=2.1" ], "system": {}, diff --git a/test/test_skill.py b/test/test_skill.py index 2e12c5e..cc833ed 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -32,23 +32,37 @@ from ovos_bus_client import Message from neon_minerva.tests.skill_unit_test_base import SkillTestCase +from neon_skill_speed_test.models import SpeedTestResult + class TestSkillMethods(SkillTestCase): def test_handle_run_speed_test(self): on_notification_set = Mock() on_notification_clear = Mock() - self.skill.bus.on("ovos.notification.api.set.controlled", - on_notification_set) - self.skill.bus.on("ovos.notification.api.remove.controlled", - on_notification_clear) + self.skill.bus.on( + "ovos.notification.api.set.controlled", on_notification_set + ) + self.skill.bus.on( + "ovos.notification.api.remove.controlled", on_notification_clear + ) self.skill.handle_run_speed_test(Message("test")) self.skill.speak_dialog.assert_any_call("start_test") args = self.skill.speak_dialog.call_args self.assertEqual(args[0][0], "results") - self.assertEqual(set(args[0][1].keys()), {'down', 'up', 'ping'}) + self.assertEqual(set(args[0][1].keys()), {"down", "up", "ping"}) on_notification_set.assert_called_once() on_notification_clear.assert_called_once() + def test_api_run_speed_test(self): + result = self.skill.run_speed_test() + self.assertIsInstance(result, SpeedTestResult) + self.assertIsInstance(result.download, float) + self.assertGreater(result.download, 1000) + self.assertIsInstance(result.upload, float) + self.assertGreater(result.upload, 1000) + self.assertIsInstance(result.ping, float) + self.assertLess(result.ping, 1000) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()