ovos-phal-plugin-mac 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: ovos-phal-plugin-mac
3
+ Version: 0.1.0
4
+ Summary: # PHAL-plugin-mac
5
+ Author-email: Mike Gray <mike@oscillatelabs.net>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac
8
+ Project-URL: Repository, https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac
9
+ Keywords: ovos,plugin,voice,assistant
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: ovos-utils
13
+ Requires-Dist: ovos-bus-client
14
+ Requires-Dist: ovos-workshop
15
+ Requires-Dist: ovos-plugin-manager
16
+ Requires-Dist: osascript==2020.12.3
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+ Requires-Dist: pytest-cov; extra == "test"
20
+
21
+ # PHAL-plugin-mac
22
+
23
+ Provides system specific commands to OVOS for Mac OS. Creates fake ducking for OCP/ovos-media, barge-in volume adjustment, GUI button compatability, and allows for management of OVOS services.
24
+
25
+ Tested on Mac OS Sonoma 14.6.1, but should be valid for all currently supported Mac OS versions as of August 2024.
26
+
27
+ ## Install
28
+
29
+ `pip install PHAL-plugin-mac`
30
+
31
+ Requires associated skill for volume-based voice commands:
32
+
33
+ - skill-ovos-volume
34
+
35
+ ## Config
36
+
37
+ This plugin is not an Admin plugin, but in order for most of the system level commands to work, the user must be in the sudoers file. This can be done by running the following command in the terminal:
38
+
39
+ `sudo vim /private/etc/sudoers.d/<username>`
40
+ Replace <username> with the username of the user running the OVOS instance.
41
+
42
+ Then add the following lines to the file:
43
+
44
+ ```sh
45
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/systemsetup
46
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/shutdown
47
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/sntp
48
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/defaults
49
+ ```
50
+
51
+ Be sure to replace `<username>` with the username of the user running the OVOS instance.
52
+
53
+ **NOTE:** Do this at your own risk. This is a security risk and should only be done if you understand the implications.
54
+
55
+ ## Handle bus events to interact with the OS
56
+
57
+ ```python
58
+ # System
59
+ self.bus.on("system.ntp.sync", self.handle_ntp_sync_request)
60
+ self.bus.on("system.ssh.status", self.handle_ssh_status)
61
+ self.bus.on("system.ssh.enable", self.handle_ssh_enable_request)
62
+ self.bus.on("system.ssh.disable", self.handle_ssh_disable_request)
63
+ self.bus.on("system.reboot", self.handle_reboot_request)
64
+ self.bus.on("system.shutdown", self.handle_shutdown_request)
65
+ self.bus.on("system.configure.language", self.handle_configure_language_request)
66
+ self.bus.on("system.mycroft.service.restart", self.handle_mycroft_restart_request)
67
+ # Volume
68
+ self.bus.on("mycroft.volume.get", self.handle_volume_set)
69
+ self.bus.on("mycroft.volume.set", self.handle_volume_set)
70
+ self.bus.on("mycroft.volume.decrease", self.handle_volume_decrease)
71
+ self.bus.on("mycroft.volume.increase", self.handle_volume_increase)
72
+ self.bus.on("mycroft.volume.mute", self.handle_volume_mute)
73
+ self.bus.on("mycroft.volume.unmute", self.handle_volume_unmute)
74
+ self.bus.on("mycroft.volume.mute.toggle", self.handle_volume_mute_toggle)
75
+ ```
76
+
77
+ ## Credits
78
+
79
+ Oscillate Labs (@mikejgray)
@@ -0,0 +1,59 @@
1
+ # PHAL-plugin-mac
2
+
3
+ Provides system specific commands to OVOS for Mac OS. Creates fake ducking for OCP/ovos-media, barge-in volume adjustment, GUI button compatability, and allows for management of OVOS services.
4
+
5
+ Tested on Mac OS Sonoma 14.6.1, but should be valid for all currently supported Mac OS versions as of August 2024.
6
+
7
+ ## Install
8
+
9
+ `pip install PHAL-plugin-mac`
10
+
11
+ Requires associated skill for volume-based voice commands:
12
+
13
+ - skill-ovos-volume
14
+
15
+ ## Config
16
+
17
+ This plugin is not an Admin plugin, but in order for most of the system level commands to work, the user must be in the sudoers file. This can be done by running the following command in the terminal:
18
+
19
+ `sudo vim /private/etc/sudoers.d/<username>`
20
+ Replace <username> with the username of the user running the OVOS instance.
21
+
22
+ Then add the following lines to the file:
23
+
24
+ ```sh
25
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/systemsetup
26
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/shutdown
27
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/sntp
28
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/defaults
29
+ ```
30
+
31
+ Be sure to replace `<username>` with the username of the user running the OVOS instance.
32
+
33
+ **NOTE:** Do this at your own risk. This is a security risk and should only be done if you understand the implications.
34
+
35
+ ## Handle bus events to interact with the OS
36
+
37
+ ```python
38
+ # System
39
+ self.bus.on("system.ntp.sync", self.handle_ntp_sync_request)
40
+ self.bus.on("system.ssh.status", self.handle_ssh_status)
41
+ self.bus.on("system.ssh.enable", self.handle_ssh_enable_request)
42
+ self.bus.on("system.ssh.disable", self.handle_ssh_disable_request)
43
+ self.bus.on("system.reboot", self.handle_reboot_request)
44
+ self.bus.on("system.shutdown", self.handle_shutdown_request)
45
+ self.bus.on("system.configure.language", self.handle_configure_language_request)
46
+ self.bus.on("system.mycroft.service.restart", self.handle_mycroft_restart_request)
47
+ # Volume
48
+ self.bus.on("mycroft.volume.get", self.handle_volume_set)
49
+ self.bus.on("mycroft.volume.set", self.handle_volume_set)
50
+ self.bus.on("mycroft.volume.decrease", self.handle_volume_decrease)
51
+ self.bus.on("mycroft.volume.increase", self.handle_volume_increase)
52
+ self.bus.on("mycroft.volume.mute", self.handle_volume_mute)
53
+ self.bus.on("mycroft.volume.unmute", self.handle_volume_unmute)
54
+ self.bus.on("mycroft.volume.mute.toggle", self.handle_volume_mute_toggle)
55
+ ```
56
+
57
+ ## Credits
58
+
59
+ Oscillate Labs (@mikejgray)
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.1
2
+ Name: ovos-phal-plugin-mac
3
+ Version: 0.1.0
4
+ Summary: # PHAL-plugin-mac
5
+ Author-email: Mike Gray <mike@oscillatelabs.net>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac
8
+ Project-URL: Repository, https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac
9
+ Keywords: ovos,plugin,voice,assistant
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: ovos-utils
13
+ Requires-Dist: ovos-bus-client
14
+ Requires-Dist: ovos-workshop
15
+ Requires-Dist: ovos-plugin-manager
16
+ Requires-Dist: osascript==2020.12.3
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+ Requires-Dist: pytest-cov; extra == "test"
20
+
21
+ # PHAL-plugin-mac
22
+
23
+ Provides system specific commands to OVOS for Mac OS. Creates fake ducking for OCP/ovos-media, barge-in volume adjustment, GUI button compatability, and allows for management of OVOS services.
24
+
25
+ Tested on Mac OS Sonoma 14.6.1, but should be valid for all currently supported Mac OS versions as of August 2024.
26
+
27
+ ## Install
28
+
29
+ `pip install PHAL-plugin-mac`
30
+
31
+ Requires associated skill for volume-based voice commands:
32
+
33
+ - skill-ovos-volume
34
+
35
+ ## Config
36
+
37
+ This plugin is not an Admin plugin, but in order for most of the system level commands to work, the user must be in the sudoers file. This can be done by running the following command in the terminal:
38
+
39
+ `sudo vim /private/etc/sudoers.d/<username>`
40
+ Replace <username> with the username of the user running the OVOS instance.
41
+
42
+ Then add the following lines to the file:
43
+
44
+ ```sh
45
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/systemsetup
46
+ <username> ALL=(ALL) NOPASSWD: /usr/sbin/shutdown
47
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/sntp
48
+ <username> ALL=(ALL) NOPASSWD: /usr/bin/defaults
49
+ ```
50
+
51
+ Be sure to replace `<username>` with the username of the user running the OVOS instance.
52
+
53
+ **NOTE:** Do this at your own risk. This is a security risk and should only be done if you understand the implications.
54
+
55
+ ## Handle bus events to interact with the OS
56
+
57
+ ```python
58
+ # System
59
+ self.bus.on("system.ntp.sync", self.handle_ntp_sync_request)
60
+ self.bus.on("system.ssh.status", self.handle_ssh_status)
61
+ self.bus.on("system.ssh.enable", self.handle_ssh_enable_request)
62
+ self.bus.on("system.ssh.disable", self.handle_ssh_disable_request)
63
+ self.bus.on("system.reboot", self.handle_reboot_request)
64
+ self.bus.on("system.shutdown", self.handle_shutdown_request)
65
+ self.bus.on("system.configure.language", self.handle_configure_language_request)
66
+ self.bus.on("system.mycroft.service.restart", self.handle_mycroft_restart_request)
67
+ # Volume
68
+ self.bus.on("mycroft.volume.get", self.handle_volume_set)
69
+ self.bus.on("mycroft.volume.set", self.handle_volume_set)
70
+ self.bus.on("mycroft.volume.decrease", self.handle_volume_decrease)
71
+ self.bus.on("mycroft.volume.increase", self.handle_volume_increase)
72
+ self.bus.on("mycroft.volume.mute", self.handle_volume_mute)
73
+ self.bus.on("mycroft.volume.unmute", self.handle_volume_unmute)
74
+ self.bus.on("mycroft.volume.mute.toggle", self.handle_volume_mute_toggle)
75
+ ```
76
+
77
+ ## Credits
78
+
79
+ Oscillate Labs (@mikejgray)
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ ovos_phal_plugin_mac.egg-info/PKG-INFO
4
+ ovos_phal_plugin_mac.egg-info/SOURCES.txt
5
+ ovos_phal_plugin_mac.egg-info/dependency_links.txt
6
+ ovos_phal_plugin_mac.egg-info/entry_points.txt
7
+ ovos_phal_plugin_mac.egg-info/requires.txt
8
+ ovos_phal_plugin_mac.egg-info/top_level.txt
9
+ phal_plugin_mac/__init__.py
10
+ phal_plugin_mac/version.py
11
+ requirements/requirements-dev.txt
12
+ requirements/requirements.txt
13
+ test/test_plugin.py
@@ -0,0 +1,2 @@
1
+ [ovos.plugin.phal]
2
+ ovos-phal-plugin-mac = ovos_phal_plugin_mac:MacOSPlugin
@@ -0,0 +1,9 @@
1
+ ovos-utils
2
+ ovos-bus-client
3
+ ovos-workshop
4
+ ovos-plugin-manager
5
+ osascript==2020.12.3
6
+
7
+ [test]
8
+ pytest
9
+ pytest-cov
@@ -0,0 +1 @@
1
+ ovos_phal_plugin_mac
@@ -0,0 +1,239 @@
1
+ """macOS PHAL plugin for OVOS."""
2
+
3
+ import subprocess
4
+
5
+ import osascript
6
+ from ovos_bus_client import Message
7
+ from ovos_plugin_manager.phal import PHALPlugin
8
+
9
+
10
+ class MacOSPlugin(PHALPlugin):
11
+ """macOS PHAL plugin for OVOS."""
12
+
13
+ def __init__(self, bus=None, config=None, *args, **kwargs):
14
+ super().__init__(bus=bus, config=config, name="ovos-PHAL-plugin-mac", *args, **kwargs)
15
+ # System events
16
+ self.bus.on("system.ntp.sync", self.handle_ntp_sync_request)
17
+ self.bus.on("system.ssh.status", self.handle_ssh_status)
18
+ self.bus.on("system.ssh.enable", self.handle_ssh_enable_request)
19
+ self.bus.on("system.ssh.disable", self.handle_ssh_disable_request)
20
+ self.bus.on("system.reboot", self.handle_reboot_request)
21
+ self.bus.on("system.shutdown", self.handle_shutdown_request)
22
+ self.bus.on("system.configure.language", self.handle_configure_language_request)
23
+ self.bus.on("system.mycroft.service.restart", self.handle_mycroft_restart_request)
24
+ # Volume events
25
+ self.bus.on("mycroft.volume.get", self.handle_volume_get)
26
+ self.bus.on("mycroft.volume.set", self.handle_volume_set)
27
+ self.bus.on("mycroft.volume.decrease", self.handle_volume_decrease)
28
+ self.bus.on("mycroft.volume.increase", self.handle_volume_increase)
29
+ self.bus.on("mycroft.volume.mute", self.handle_volume_mute)
30
+ self.bus.on("mycroft.volume.unmute", self.handle_volume_unmute)
31
+ self.bus.on("mycroft.volume.mute.toggle", self.handle_volume_mute_toggle)
32
+
33
+ @property
34
+ def allow_reboot(self):
35
+ """Check if reboot is allowed."""
36
+ return self.config.get("allow_reboot", True)
37
+
38
+ @property
39
+ def allow_shutdown(self):
40
+ """Check if shutdown is allowed."""
41
+ return self.config.get("allow_shutdown", True)
42
+
43
+ @property
44
+ def volume_change_interval(self):
45
+ """Get the volume change interval percentage. Defaults to 10."""
46
+ return self.config.get("volume_change_interval", 10)
47
+
48
+ def _run_command(self, command, check=True):
49
+ """Private method to run shell commands."""
50
+ try:
51
+ return subprocess.run(command, check=check, capture_output=True, text=True)
52
+ except Exception as err:
53
+ self.log.exception("Error running command: %s", err)
54
+
55
+ def _run_applescript(self, script):
56
+ """Private method to run AppleScript."""
57
+ return_code, out, err = osascript.run(script)
58
+ self.log.debug("Return code for %s was %s", script, return_code)
59
+ if return_code and return_code > 0:
60
+ self.log.error("Error code %s running AppleScript: %s", return_code, err)
61
+ return
62
+ return out
63
+
64
+ def _set_volume(self, volume):
65
+ """Set the system volume (0-100)."""
66
+ script = f"set volume output volume {volume}"
67
+ self._run_applescript(script)
68
+
69
+ def _get_volume(self):
70
+ """Get the current system volume (0-100)."""
71
+ script = "output volume of (get volume settings)"
72
+ result = self._run_applescript(script)
73
+ self.log.debug("Current volume: %s", result)
74
+ return int(result) if result else None
75
+
76
+ def _is_muted(self):
77
+ """Check if the system is muted."""
78
+ script = "output muted of (get volume settings)"
79
+ result = self._run_applescript(script)
80
+ if not result:
81
+ return False
82
+ return "true" in result.lower()
83
+
84
+ def _set_mute(self, mute):
85
+ """Set the system mute state."""
86
+ script = "set volume with output muted"
87
+ if not mute:
88
+ script = "set volume without output muted"
89
+ self._run_applescript(script)
90
+
91
+ def handle_volume_get(self, message: Message):
92
+ """Handle the volume get request."""
93
+ volume = self._get_volume()
94
+ if volume:
95
+ self.bus.emit(message.reply("mycroft.volume.get.response", {"percent": volume}))
96
+ else:
97
+ self.log.error("Error getting Mac volume")
98
+
99
+ def handle_volume_set(self, message: Message):
100
+ """Handle the volume set request."""
101
+ volume = message.data.get("percent", 50)
102
+ volume = max(0, min(100, volume)) # Ensure volume is between 0 and 100
103
+ self._set_volume(volume)
104
+ self.bus.emit(message.forward("mycroft.volume.set.confirm", {"percent": volume}))
105
+
106
+ def handle_volume_decrease(self, message: Message):
107
+ """Handle the volume decrease request."""
108
+ current_volume = self._get_volume()
109
+ new_volume = max(
110
+ 0, current_volume - self.volume_change_interval
111
+ ) # Decrease by volume_change_interval, but not below 0
112
+ self._set_volume(new_volume)
113
+ self.bus.emit(message.forward("mycroft.volume.set.confirm", {"percent": new_volume}))
114
+
115
+ def handle_volume_increase(self, message: Message):
116
+ """Handle the volume increase request."""
117
+ current_volume = self._get_volume()
118
+ new_volume = min(
119
+ 100, current_volume + self.volume_change_interval
120
+ ) # Increase by volume_change_interval, but not above 100
121
+ self._set_volume(new_volume)
122
+ self.bus.emit(message.forward("mycroft.volume.set.confirm", {"percent": new_volume}))
123
+
124
+ def handle_volume_mute(self, message: Message):
125
+ """Handle the volume mute request."""
126
+ self._set_mute(True)
127
+ self.bus.emit(message.forward("mycroft.volume.mute.confirm", {"muted": True}))
128
+
129
+ def handle_volume_unmute(self, message: Message):
130
+ """Handle the volume unmute request."""
131
+ self._set_mute(False)
132
+ self.bus.emit(message.forward("mycroft.volume.mute.confirm", {"muted": False}))
133
+
134
+ def handle_volume_mute_toggle(self, message: Message):
135
+ """Handle the volume mute toggle request."""
136
+ current_mute = self._is_muted()
137
+ self._set_mute(not current_mute)
138
+ self.bus.emit(message.forward("mycroft.volume.mute.confirm", {"muted": not current_mute}))
139
+
140
+ def _get_ntp_server(self):
141
+ """Private method to get the configured NTP server."""
142
+ try:
143
+ result = self._run_command(["systemsetup", "-getnetworktimeserver"])
144
+ return result.stdout.strip().split(": ")[-1]
145
+ except Exception as err:
146
+ self.log.exception("Error getting NTP server: %s", err)
147
+ return
148
+
149
+ def handle_ntp_sync_request(self, message: Message):
150
+ """Handle the NTP sync request."""
151
+ try:
152
+ ntp_server = self._get_ntp_server()
153
+ self._run_command(["sntp", "-sS", ntp_server])
154
+ self.bus.emit(message.forward("system.ntp.sync.complete"))
155
+ except subprocess.CalledProcessError:
156
+ self.bus.emit(message.forward("system.ntp.sync.failed"))
157
+
158
+ def handle_ssh_status(self, message: Message):
159
+ """Handle the SSH status request."""
160
+ status = self._run_command(["systemsetup", "-getremotelogin"])
161
+ is_enabled = "On" in status.stdout
162
+ self.bus.emit(message.forward("system.ssh.status.response", {"enabled": is_enabled}))
163
+
164
+ def handle_ssh_enable_request(self, message: Message):
165
+ """Handle the SSH enable request."""
166
+ try:
167
+ script = """
168
+ tell application "System Events"
169
+ activate
170
+ display dialog "OVOS needs Full Disk Access to enable Remote Login. Please grant permission in System Preferences." buttons {"OK"} default button "OK"
171
+ end tell
172
+ """
173
+ self._run_applescript(script)
174
+ self._run_command(["systemsetup", "-setremotelogin", "on"])
175
+ self.bus.emit(message.forward("system.ssh.enabled"))
176
+ except subprocess.CalledProcessError:
177
+ self.bus.emit(message.forward("system.ssh.enable.failed"))
178
+
179
+ def handle_ssh_disable_request(self, message: Message):
180
+ """Handle the SSH disable request."""
181
+ try:
182
+ script = """
183
+ tell application "System Events"
184
+ activate
185
+ display dialog "OVOS needs Full Disk Access to disable Remote Login. Please grant permission in System Preferences." buttons {"OK"} default button "OK"
186
+ end tell
187
+ """
188
+ self._run_applescript(script)
189
+ self._run_command(["systemsetup", "-setremotelogin", "off"])
190
+ self.bus.emit(message.forward("system.ssh.disabled"))
191
+ except subprocess.CalledProcessError:
192
+ self.bus.emit(message.forward("system.ssh.disable.failed"))
193
+
194
+ def handle_reboot_request(self, message: Message):
195
+ """Handle the reboot request."""
196
+ if self.allow_reboot is False:
197
+ self.bus.emit(message.forward("system.reboot.failed"))
198
+ try:
199
+ self._run_command(["shutdown", "-r", "now"])
200
+ except subprocess.CalledProcessError:
201
+ self.bus.emit(message.forward("system.reboot.failed"))
202
+
203
+ def handle_shutdown_request(self, message: Message):
204
+ """Handle the shutdown request."""
205
+ if self.allow_shutdown is False:
206
+ self.bus.emit(message.forward("system.shutdown.failed"))
207
+ try:
208
+ self._run_command(["shutdown", "-h", "now"])
209
+ except subprocess.CalledProcessError:
210
+ self.bus.emit(message.forward("system.shutdown.failed"))
211
+
212
+ def handle_configure_language_request(self, message: Message):
213
+ """Handle the configure language request."""
214
+ lang = message.data.get("lang")
215
+ if lang:
216
+ try:
217
+ self._run_command(["defaults", "write", "NSGlobalDomain", "AppleLanguages", f'("{lang}")'])
218
+ self.bus.emit(message.forward("system.language.configured", {"lang": lang}))
219
+ except subprocess.CalledProcessError:
220
+ self.bus.emit(message.forward("system.language.configure.failed"))
221
+ else:
222
+ self.bus.emit(message.forward("system.language.configure.failed", {"error": "Language not specified"}))
223
+
224
+ def handle_mycroft_restart_request(self, message: Message):
225
+ """Handle the Mycroft restart request."""
226
+ try:
227
+ self._run_command(["launchctl", "stop", "com.ovos.service"])
228
+ self._run_command(["launchctl", "start", "com.ovos.service"])
229
+ self.bus.emit(message.forward("system.mycroft.service.restarted"))
230
+ except subprocess.CalledProcessError as err:
231
+ self.log.exception("OVOS service request restart failed", err)
232
+ self.bus.emit(message.forward("system.mycroft.service.restart.failed"))
233
+
234
+
235
+ if __name__ == "__main__":
236
+ from ovos_utils.fakebus import FakeBus
237
+
238
+ plugin = MacOSPlugin(bus=FakeBus())
239
+ print("BREAK")
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0" # This gets updated automatically by release-please
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ovos-phal-plugin-mac"
7
+ readme = "README.md"
8
+ authors = [{ name = "Mike Gray", email = "mike@oscillatelabs.net" }]
9
+ license = { text = "Apache-2.0" }
10
+ keywords = ["ovos", "plugin", "voice", "assistant"]
11
+ requires-python = ">=3.9"
12
+ dynamic = ["version", "dependencies", "optional-dependencies", "description"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac"
16
+ Repository = "https://github.com/OscillateLabsLLC/ovos-phal-plugin-mac"
17
+
18
+ [tool.setuptools]
19
+ package-dir = { "ovos_phal_plugin_mac" = "phal_plugin_mac" }
20
+ packages = ["ovos_phal_plugin_mac"]
21
+ include-package-data = true
22
+
23
+ [tool.setuptools.dynamic]
24
+ dependencies = { file = ["requirements/requirements.txt"] }
25
+ optional-dependencies = { test = { file = [
26
+ "requirements/requirements-dev.txt",
27
+ ] } }
28
+ description = { file = "README.md" }
29
+ version = { attr = "phal_plugin_mac.version.__version__" }
30
+
31
+ [tool.setuptools.package-data]
32
+ ovos_phal_plugin_mac = [
33
+ "*.json",
34
+ "locale/*",
35
+ "intents/*",
36
+ "dialog/*",
37
+ "vocab/*",
38
+ "regex/*",
39
+ "ui/*",
40
+ "requirements/*",
41
+ ]
42
+
43
+ [project.entry-points."ovos.plugin.phal"]
44
+ ovos-phal-plugin-mac = "ovos_phal_plugin_mac:MacOSPlugin"
45
+
46
+ [tool.uv]
47
+ package = true
@@ -0,0 +1,2 @@
1
+ pytest
2
+ pytest-cov
@@ -0,0 +1,5 @@
1
+ ovos-utils
2
+ ovos-bus-client
3
+ ovos-workshop
4
+ ovos-plugin-manager
5
+ osascript==2020.12.3
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,330 @@
1
+ # pylint: disable=missing-docstring,redefined-outer-name,protected-access,unnecessary-lambda
2
+ import subprocess
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+ from ovos_bus_client import Message
7
+ from ovos_plugin_manager.phal import find_phal_plugins
8
+ from ovos_utils.fakebus import FakeBus
9
+
10
+ from phal_plugin_mac import MacOSPlugin
11
+
12
+
13
+ @pytest.fixture
14
+ def bus():
15
+ return FakeBus()
16
+
17
+
18
+ @pytest.fixture
19
+ def plugin(bus):
20
+ config = {"allow_reboot": True, "allow_shutdown": True, "volume_change_interval": 10}
21
+ return MacOSPlugin(bus=bus, config=config)
22
+
23
+
24
+ @pytest.fixture
25
+ def message():
26
+ return Message("test.message")
27
+
28
+
29
+ def test_find_phal_plugins():
30
+ plugins = find_phal_plugins()
31
+ assert "ovos-phal-plugin-mac" in plugins
32
+
33
+
34
+ def test_init(plugin):
35
+ assert plugin.allow_reboot is True
36
+ assert plugin.allow_shutdown is True
37
+ assert plugin.volume_change_interval == 10
38
+
39
+
40
+ def test_run_command_error(plugin):
41
+ cmd_results = plugin._run_command(["rm", "-r", "/ae5ih7srjo8g4ege5"])
42
+ assert not isinstance(cmd_results, subprocess.CompletedProcess)
43
+ assert cmd_results is None
44
+
45
+
46
+ @patch("subprocess.run")
47
+ def test_get_ntp_server(mock_run, plugin):
48
+ mock_run.return_value = MagicMock(stdout="Network Time Server: test.ntp.org\n")
49
+ assert plugin._get_ntp_server() == "test.ntp.org"
50
+
51
+
52
+ @patch("subprocess.run")
53
+ def test_get_ntp_server_error(mock_run, plugin):
54
+ mock_run.side_effect = subprocess.CalledProcessError(1, "test")
55
+ assert plugin._get_ntp_server() is None
56
+
57
+
58
+ @patch("phal_plugin_mac.MacOSPlugin._run_applescript")
59
+ def test_handle_ssh_enable_request(mock_run_applescript, plugin, message, bus):
60
+ received_messages = []
61
+ bus.on("system.ssh.enabled", lambda m: received_messages.append(m))
62
+
63
+ plugin.handle_ssh_enable_request(message)
64
+
65
+ mock_run_applescript.assert_called()
66
+ assert len(received_messages) == 1
67
+
68
+
69
+ @patch("phal_plugin_mac.MacOSPlugin._run_applescript")
70
+ def test_handle_ssh_disable_request(mock_run_applescript, plugin, message, bus):
71
+ received_messages = []
72
+ bus.on("system.ssh.disabled", lambda m: received_messages.append(m))
73
+
74
+ plugin.handle_ssh_disable_request(message)
75
+
76
+ mock_run_applescript.assert_called()
77
+ assert len(received_messages) == 1
78
+
79
+
80
+ def test_handle_reboot_request_not_allowed(plugin, message, bus):
81
+ plugin.config["allow_reboot"] = False
82
+ received_messages = []
83
+ bus.on("system.reboot.failed", lambda m: received_messages.append(m))
84
+
85
+ plugin.handle_reboot_request(message)
86
+
87
+ assert len(received_messages) == 1
88
+
89
+
90
+ def test_handle_shutdown_request_not_allowed(plugin, message, bus):
91
+ plugin.config["allow_shutdown"] = False
92
+ received_messages = []
93
+ bus.on("system.shutdown.failed", lambda m: received_messages.append(m))
94
+
95
+ plugin.handle_shutdown_request(message)
96
+
97
+ assert len(received_messages) == 1
98
+
99
+
100
+ @patch.object(MacOSPlugin, "_run_command")
101
+ def test_handle_configure_language_request_error(mock_run_command, plugin, message, bus):
102
+ mock_run_command.side_effect = subprocess.CalledProcessError(1, "language config failure")
103
+ received_messages = []
104
+ bus.on("system.language.configure.failed", lambda m: received_messages.append(m))
105
+
106
+ message.data["lang"] = "en-US"
107
+ plugin.handle_configure_language_request(message)
108
+
109
+ assert len(received_messages) == 1
110
+ # Optionally, check the content of the received message
111
+ # assert received_messages[0].data == {"error": "Command execution failed"}
112
+
113
+
114
+ @patch.object(MacOSPlugin, "_run_command")
115
+ def test_handle_mycroft_restart_request_error(mock_run_command, plugin, message, bus):
116
+ mock_run_command.side_effect = subprocess.CalledProcessError(1, "mycroft restart request error")
117
+ received_messages = []
118
+ bus.on("system.mycroft.service.restart.failed", lambda m: received_messages.append(m))
119
+
120
+ plugin.handle_mycroft_restart_request(message)
121
+ assert len(received_messages) == 1
122
+
123
+
124
+ @patch.object(MacOSPlugin, "_run_command")
125
+ def test_handle_ntp_sync_request_error(mock_run_command, plugin, message, bus):
126
+ mock_run_command.side_effect = subprocess.CalledProcessError(1, "ntp sync request error")
127
+ received_messages = []
128
+ bus.on("system.ntp.sync.failed", lambda m: received_messages.append(m))
129
+
130
+ plugin.handle_ntp_sync_request(message)
131
+
132
+ assert len(received_messages) == 1
133
+
134
+
135
+ @patch.object(MacOSPlugin, "_run_command")
136
+ def test_handle_ssh_enable_request_error(mock_run_command, plugin, message, bus):
137
+ mock_run_command.side_effect = subprocess.CalledProcessError(1, "ssh enable request error")
138
+ received_messages = []
139
+ bus.on("system.ssh.enable.failed", lambda m: received_messages.append(m))
140
+
141
+ plugin.handle_ssh_enable_request(message)
142
+
143
+ assert len(received_messages) == 1
144
+
145
+
146
+ @patch.object(MacOSPlugin, "_run_command")
147
+ def test_handle_ssh_disable_request_error(mock_run_command, plugin, message, bus):
148
+ mock_run_command.side_effect = subprocess.CalledProcessError(1, "ssh disable request error")
149
+ received_messages = []
150
+ bus.on("system.ssh.disable.failed", lambda m: received_messages.append(m))
151
+
152
+ plugin.handle_ssh_disable_request(message)
153
+
154
+ assert len(received_messages) == 1
155
+
156
+
157
+ @patch("subprocess.run")
158
+ def test_run_command(mock_run, plugin):
159
+ mock_run.return_value = MagicMock(stdout="test output")
160
+ result = plugin._run_command(["test", "command"])
161
+ mock_run.assert_called_once_with(["test", "command"], check=True, capture_output=True, text=True)
162
+ assert result.stdout == "test output"
163
+
164
+
165
+ @patch("phal_plugin_mac.MacOSPlugin._run_applescript")
166
+ def test_get_volume(mock_run_applescript, plugin):
167
+ mock_run_applescript.return_value = 50
168
+ volume = plugin._get_volume()
169
+ mock_run_applescript.assert_called_once_with("output volume of (get volume settings)")
170
+ assert volume == 50
171
+
172
+
173
+ @patch("phal_plugin_mac.MacOSPlugin._run_applescript")
174
+ def test_set_volume(mock_run_applescript, plugin):
175
+ plugin._set_volume(75)
176
+ mock_run_applescript.assert_called_once_with("set volume output volume 75")
177
+
178
+
179
+ @patch("phal_plugin_mac.MacOSPlugin._run_applescript")
180
+ def test_is_muted(mock_run_applescript, plugin):
181
+ mock_run_applescript.return_value = "true"
182
+ assert plugin._is_muted() is True
183
+ mock_run_applescript.assert_called_once_with("output muted of (get volume settings)")
184
+
185
+
186
+ @patch("phal_plugin_mac.MacOSPlugin._set_volume")
187
+ def test_handle_volume_set(mock_set_volume, plugin, message, bus):
188
+ received_messages = []
189
+ bus.on("mycroft.volume.set.confirm", lambda m: received_messages.append(m))
190
+
191
+ message.data["percent"] = 60
192
+ plugin.handle_volume_set(message)
193
+
194
+ mock_set_volume.assert_called_once_with(60)
195
+ assert len(received_messages) == 1
196
+ assert received_messages[0].data["percent"] == 60
197
+
198
+
199
+ @patch("phal_plugin_mac.MacOSPlugin._get_volume")
200
+ @patch("phal_plugin_mac.MacOSPlugin._set_volume")
201
+ def test_handle_volume_decrease(mock_set_volume, mock_get_volume, plugin, message, bus):
202
+ received_messages = []
203
+ bus.on("mycroft.volume.set.confirm", lambda m: received_messages.append(m))
204
+
205
+ mock_get_volume.return_value = 50
206
+ plugin.handle_volume_decrease(message)
207
+
208
+ mock_set_volume.assert_called_once_with(40)
209
+ assert len(received_messages) == 1
210
+ assert received_messages[0].data["percent"] == 40
211
+
212
+
213
+ @patch("phal_plugin_mac.MacOSPlugin._get_volume")
214
+ @patch("phal_plugin_mac.MacOSPlugin._set_volume")
215
+ def test_handle_volume_increase(mock_set_volume, mock_get_volume, plugin, message, bus):
216
+ received_messages = []
217
+ bus.on("mycroft.volume.set.confirm", lambda m: received_messages.append(m))
218
+
219
+ mock_get_volume.return_value = 50
220
+ plugin.handle_volume_increase(message)
221
+
222
+ mock_set_volume.assert_called_once_with(60)
223
+ assert len(received_messages) == 1
224
+ assert received_messages[0].data["percent"] == 60
225
+
226
+
227
+ @patch("phal_plugin_mac.MacOSPlugin._set_mute")
228
+ def test_handle_volume_mute(mock_set_mute, plugin, message, bus):
229
+ received_messages = []
230
+ bus.on("mycroft.volume.mute.confirm", lambda m: received_messages.append(m))
231
+
232
+ plugin.handle_volume_mute(message)
233
+
234
+ mock_set_mute.assert_called_once_with(True)
235
+ assert len(received_messages) == 1
236
+ assert received_messages[0].data["muted"] is True
237
+
238
+
239
+ @patch("phal_plugin_mac.MacOSPlugin._set_mute")
240
+ def test_handle_volume_unmute(mock_set_mute, plugin, message, bus):
241
+ received_messages = []
242
+ bus.on("mycroft.volume.mute.confirm", lambda m: received_messages.append(m))
243
+
244
+ plugin.handle_volume_unmute(message)
245
+
246
+ mock_set_mute.assert_called_once_with(False)
247
+ assert len(received_messages) == 1
248
+ assert not received_messages[0].data["muted"]
249
+
250
+
251
+ @patch("phal_plugin_mac.MacOSPlugin._is_muted")
252
+ @patch("phal_plugin_mac.MacOSPlugin._set_mute")
253
+ def test_handle_volume_mute_toggle(mock_set_mute, mock_is_muted, plugin, message, bus):
254
+ received_messages = []
255
+ bus.on("mycroft.volume.mute.confirm", lambda m: received_messages.append(m))
256
+
257
+ mock_is_muted.return_value = True
258
+ plugin.handle_volume_mute_toggle(message)
259
+
260
+ mock_set_mute.assert_called_once_with(False)
261
+ assert len(received_messages) == 1
262
+ assert not received_messages[0].data["muted"]
263
+
264
+
265
+ @patch("subprocess.run")
266
+ def test_handle_ntp_sync_request(mock_run, plugin, message, bus):
267
+ received_messages = []
268
+ bus.on("system.ntp.sync.complete", lambda m: received_messages.append(m))
269
+
270
+ mock_run.return_value = MagicMock(stdout="Network Time Server: time.apple.com\n")
271
+ plugin.handle_ntp_sync_request(message)
272
+
273
+ mock_run.assert_any_call(["systemsetup", "-getnetworktimeserver"], check=True, capture_output=True, text=True)
274
+ mock_run.assert_any_call(["sntp", "-sS", "time.apple.com"], check=True, capture_output=True, text=True)
275
+ assert len(received_messages) == 1
276
+
277
+
278
+ @patch("subprocess.run")
279
+ def test_handle_ssh_status(mock_run, plugin, message, bus):
280
+ received_messages = []
281
+ bus.on("system.ssh.status.response", lambda m: received_messages.append(m))
282
+
283
+ mock_run.return_value = MagicMock(stdout="Remote Login: On\n")
284
+ plugin.handle_ssh_status(message)
285
+
286
+ mock_run.assert_called_once_with(["systemsetup", "-getremotelogin"], check=True, capture_output=True, text=True)
287
+ assert len(received_messages) == 1
288
+ assert received_messages[0].data["enabled"] is True
289
+
290
+
291
+ @patch("subprocess.run")
292
+ def test_handle_reboot_request(mock_run, plugin, message):
293
+ plugin.handle_reboot_request(message)
294
+ mock_run.assert_called_once_with(["shutdown", "-r", "now"], check=True, capture_output=True, text=True)
295
+
296
+
297
+ @patch("subprocess.run")
298
+ def test_handle_shutdown_request(mock_run, plugin, message):
299
+ plugin.handle_shutdown_request(message)
300
+ mock_run.assert_called_once_with(["shutdown", "-h", "now"], check=True, capture_output=True, text=True)
301
+
302
+
303
+ @patch("subprocess.run")
304
+ def test_handle_configure_language_request(mock_run, plugin, message, bus):
305
+ received_messages = []
306
+ bus.on("system.language.configured", lambda m: received_messages.append(m))
307
+
308
+ message.data["lang"] = "en-US"
309
+ plugin.handle_configure_language_request(message)
310
+
311
+ mock_run.assert_called_once_with(
312
+ ["defaults", "write", "NSGlobalDomain", "AppleLanguages", '("en-US")'],
313
+ check=True,
314
+ capture_output=True,
315
+ text=True,
316
+ )
317
+ assert len(received_messages) == 1
318
+ assert received_messages[0].data["lang"] == "en-US"
319
+
320
+
321
+ @patch("subprocess.run")
322
+ def test_handle_mycroft_restart_request(mock_run, plugin, message, bus):
323
+ received_messages = []
324
+ bus.on("system.mycroft.service.restarted", lambda m: received_messages.append(m))
325
+
326
+ plugin.handle_mycroft_restart_request(message)
327
+
328
+ mock_run.assert_any_call(["launchctl", "stop", "com.ovos.service"], check=True, capture_output=True, text=True)
329
+ mock_run.assert_any_call(["launchctl", "start", "com.ovos.service"], check=True, capture_output=True, text=True)
330
+ assert len(received_messages) == 1