1"""
2Support for Bluetooth (using BlueZ in Linux).
3
4The following packages are required packages for this module:
5
6    bluez >= 5.7
7    bluez-libs >= 5.7
8    bluez-utils >= 5.7
9    pybluez >= 0.18
10"""
11import shlex
12
13import salt.utils.validate.net
14from salt.exceptions import CommandExecutionError
15
16HAS_PYBLUEZ = False
17try:
18    import bluetooth  # pylint: disable=import-error
19
20    HAS_PYBLUEZ = True
21except ImportError:
22    pass
23
24__func_alias__ = {"address_": "address"}
25
26# Define the module's virtual name
27__virtualname__ = "bluetooth"
28
29
30def __virtual__():
31    """
32    Only load the module if bluetooth is installed
33    """
34    if HAS_PYBLUEZ:
35        return __virtualname__
36    return (
37        False,
38        "The bluetooth execution module cannot be loaded: bluetooth not installed.",
39    )
40
41
42def version():
43    """
44    Return Bluez version from bluetoothd -v
45
46    CLI Example:
47
48    .. code-block:: bash
49
50        salt '*' bluetoothd.version
51    """
52    cmd = "bluetoothctl -v"
53    out = __salt__["cmd.run"](cmd).splitlines()
54    bluez_version = out[0]
55    pybluez_version = "<= 0.18 (Unknown, but installed)"
56    try:
57        pybluez_version = bluetooth.__version__
58    except Exception as exc:  # pylint: disable=broad-except
59        pass
60    return {"Bluez": bluez_version, "PyBluez": pybluez_version}
61
62
63def address_():
64    """
65    Get the many addresses of the Bluetooth adapter
66
67    CLI Example:
68
69    .. code-block:: bash
70
71        salt '*' bluetooth.address
72    """
73    ret = {}
74    cmd = "hciconfig"
75    out = __salt__["cmd.run"](cmd).splitlines()
76    dev = ""
77    for line in out:
78        if line.startswith("hci"):
79            comps = line.split(":")
80            dev = comps[0]
81            ret[dev] = {
82                "device": dev,
83                "path": "/sys/class/bluetooth/{}".format(dev),
84            }
85        if "BD Address" in line:
86            comps = line.split()
87            ret[dev]["address"] = comps[2]
88        if "DOWN" in line:
89            ret[dev]["power"] = "off"
90        if "UP RUNNING" in line:
91            ret[dev]["power"] = "on"
92    return ret
93
94
95def power(dev, mode):
96    """
97    Power a bluetooth device on or off
98
99    CLI Examples:
100
101    .. code-block:: bash
102
103        salt '*' bluetooth.power hci0 on
104        salt '*' bluetooth.power hci0 off
105    """
106    if dev not in address_():
107        raise CommandExecutionError("Invalid dev passed to bluetooth.power")
108
109    if mode == "on" or mode is True:
110        state = "up"
111        mode = "on"
112    else:
113        state = "down"
114        mode = "off"
115    cmd = "hciconfig {} {}".format(dev, state)
116    __salt__["cmd.run"](cmd).splitlines()
117    info = address_()
118    if info[dev]["power"] == mode:
119        return True
120    return False
121
122
123def discoverable(dev):
124    """
125    Enable this bluetooth device to be discoverable.
126
127    CLI Example:
128
129    .. code-block:: bash
130
131        salt '*' bluetooth.discoverable hci0
132    """
133    if dev not in address_():
134        raise CommandExecutionError("Invalid dev passed to bluetooth.discoverable")
135
136    cmd = "hciconfig {} iscan".format(dev)
137    __salt__["cmd.run"](cmd).splitlines()
138    cmd = "hciconfig {}".format(dev)
139    out = __salt__["cmd.run"](cmd)
140    if "UP RUNNING ISCAN" in out:
141        return True
142    return False
143
144
145def noscan(dev):
146    """
147    Turn off scanning modes on this device.
148
149    CLI Example:
150
151    .. code-block:: bash
152
153        salt '*' bluetooth.noscan hci0
154    """
155    if dev not in address_():
156        raise CommandExecutionError("Invalid dev passed to bluetooth.noscan")
157
158    cmd = "hciconfig {} noscan".format(dev)
159    __salt__["cmd.run"](cmd).splitlines()
160    cmd = "hciconfig {}".format(dev)
161    out = __salt__["cmd.run"](cmd)
162    if "SCAN" in out:
163        return False
164    return True
165
166
167def scan():
168    """
169    Scan for bluetooth devices in the area
170
171    CLI Example:
172
173    .. code-block:: bash
174
175        salt '*' bluetooth.scan
176    """
177    ret = []
178    devices = bluetooth.discover_devices(lookup_names=True)
179    for device in devices:
180        ret.append({device[0]: device[1]})
181    return ret
182
183
184def block(bdaddr):
185    """
186    Block a specific bluetooth device by BD Address
187
188    CLI Example:
189
190    .. code-block:: bash
191
192        salt '*' bluetooth.block DE:AD:BE:EF:CA:FE
193    """
194    if not salt.utils.validate.net.mac(bdaddr):
195        raise CommandExecutionError("Invalid BD address passed to bluetooth.block")
196
197    cmd = "hciconfig {} block".format(bdaddr)
198    __salt__["cmd.run"](cmd).splitlines()
199
200
201def unblock(bdaddr):
202    """
203    Unblock a specific bluetooth device by BD Address
204
205    CLI Example:
206
207    .. code-block:: bash
208
209        salt '*' bluetooth.unblock DE:AD:BE:EF:CA:FE
210    """
211    if not salt.utils.validate.net.mac(bdaddr):
212        raise CommandExecutionError("Invalid BD address passed to bluetooth.unblock")
213
214    cmd = "hciconfig {} unblock".format(bdaddr)
215    __salt__["cmd.run"](cmd).splitlines()
216
217
218def pair(address, key):
219    """
220    Pair the bluetooth adapter with a device
221
222    CLI Example:
223
224    .. code-block:: bash
225
226        salt '*' bluetooth.pair DE:AD:BE:EF:CA:FE 1234
227
228    Where DE:AD:BE:EF:CA:FE is the address of the device to pair with, and 1234
229    is the passphrase.
230
231    TODO: This function is currently broken, as the bluez-simple-agent program
232    no longer ships with BlueZ >= 5.0. It needs to be refactored.
233    """
234    if not salt.utils.validate.net.mac(address):
235        raise CommandExecutionError("Invalid BD address passed to bluetooth.pair")
236
237    try:
238        int(key)
239    except Exception:  # pylint: disable=broad-except
240        raise CommandExecutionError(
241            "bluetooth.pair requires a numerical key to be used"
242        )
243
244    addy = address_()
245    cmd = "echo {} | bluez-simple-agent {} {}".format(
246        shlex.quote(addy["device"]), shlex.quote(address), shlex.quote(key)
247    )
248    out = __salt__["cmd.run"](cmd, python_shell=True).splitlines()
249    return out
250
251
252def unpair(address):
253    """
254    Unpair the bluetooth adapter from a device
255
256    CLI Example:
257
258    .. code-block:: bash
259
260        salt '*' bluetooth.unpair DE:AD:BE:EF:CA:FE
261
262    Where DE:AD:BE:EF:CA:FE is the address of the device to unpair.
263
264    TODO: This function is currently broken, as the bluez-simple-agent program
265    no longer ships with BlueZ >= 5.0. It needs to be refactored.
266    """
267    if not salt.utils.validate.net.mac(address):
268        raise CommandExecutionError("Invalid BD address passed to bluetooth.unpair")
269
270    cmd = "bluez-test-device remove {}".format(address)
271    out = __salt__["cmd.run"](cmd).splitlines()
272    return out
273
274
275def start():
276    """
277    Start the bluetooth service.
278
279    CLI Example:
280
281    .. code-block:: bash
282
283        salt '*' bluetooth.start
284    """
285    out = __salt__["service.start"]("bluetooth")
286    return out
287
288
289def stop():
290    """
291    Stop the bluetooth service.
292
293    CLI Example:
294
295    .. code-block:: bash
296
297        salt '*' bluetooth.stop
298    """
299    out = __salt__["service.stop"]("bluetooth")
300    return out
301