Skip to content

Commit fa20d2d

Browse files
committed
Allow interface to be selected
1 parent e00a8c7 commit fa20d2d

File tree

8 files changed

+134
-44
lines changed

8 files changed

+134
-44
lines changed

README.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ Enjoy and please do feel free to feedback experiences and issues.
1616
You can specify a Wi-Fi connection you would like your device to try and connect to the first time it loads by using the environment variables in the docker-compose.yml file. Once this connection is established, the device will stay connected after reboots until you use the `forget` endpoint. If the network is not available, the hotspot will start instead.
1717

1818
````
19-
PWC_SSID: "network-name" # The SSID of the network you would like to try and auto-connect.
20-
USERNAME: "username" # Optional, for enterprise networks
21-
PWC_PASSWORD: "your-password" # Optional, the password associated with the Wi-Fi network. Must be 8 characters or more.
19+
PWC_AC_SSID: "network-name" # The SSID of the network you would like to try and auto-connect.
20+
PWC_AC_USERNAME: "username" # Optional, for enterprise networks
21+
PWC_AC_PASSWORD: "your-password" # Optional, the password associated with the Wi-Fi network. Must be 8 characters or more.
2222
````
2323

2424
## Securing the API
@@ -32,6 +32,21 @@ Users will then be unable to access the API `http://your-device:9090/v1/connect`
3232

3333
Alternatively, if you would rather have your backend use specified ports instead of the host network, you can change the `PWC_HOST` environment variable to `172.17.0.1` and access the API from `http://172.17.0.1:9090/v1/connect`.
3434

35+
## Changing the default interface
36+
By default, the first available Wi-Fi interface available will be used. For the vast majority of cases there is only one Wi-Fi interface (`wlan0`) and therefore this is no issue. Similarly, if you plug in a Wi-Fi dongle to a device without its own built-in Wi-Fi, the Wi-Fi dongle will be used by default. If however, you have a device with built in Wi-Fi and a Wi-Fi dongle, you will have a device with two interfaces (usually `wlan0` and `wlan1`). For these instances, or on other occasions where you have a complex interface setup, you can specify which interface you would like Py Wi-Fi Connect to use by setting the environment variable shown in the `docker-compose.yml` file:
37+
38+
````
39+
PWC_INTERFACE: "wlan0" // Optional.
40+
````
41+
42+
To allow for automatic detection, set the variable to `false` or remove the variable from your `docker-compose.yml` file:
43+
44+
````
45+
PWC_INTERFACE: false
46+
````
47+
48+
This setting can also be controlled using the `/set_interface` endpoint.
49+
3550
## LED Indicator
3651
Some devices - such as the Raspberry Pi series - have an LED that can be controlled. When your device is connected to Wi-Fi, Python Wi-Fi Connect turns the LED on. When disconnected or in Hotspot mode, it turns the LED off.
3752

@@ -83,7 +98,7 @@ When passing `"all_networks": false` this endpoint will only touch Wi-Fi connect
8398
#### POST
8499
````
85100
{
86-
"all_networks": false
101+
"all_networks": false // Optional. Defaults to False.
87102
}
88103
````
89104

@@ -146,3 +161,25 @@ Requests are returned immediately and then the process is executed. Otherwise us
146161
"message": "ok"
147162
}
148163
````
164+
165+
### http://your-device:9090/v1//set_interface
166+
167+
By default the Wi-Fi interface is auto-detected. If you need to specify an interface, you can do so using this endpoint.
168+
169+
To set back to auto-detection, pass `false` as the value.
170+
171+
Changing the setting will only last until the next restart of the container, when it will resort back to the default setting set by the environment variable in the container.
172+
173+
#### POST
174+
````
175+
{
176+
"all_networks": "wlan0" // Optional.
177+
}
178+
````
179+
180+
#### Response status 200
181+
````
182+
{
183+
"message": "ok"
184+
}
185+
````

docker-compose.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ services:
99

1010
## Hotspot details ##
1111
PWC_HOTSPOT_SSID: "Py Wi-Fi Connect"
12-
#HOTSPOT_PASSWORD: "my-hotspot-password" # Optional hotspot password. Must be 8 characters or more.
12+
#PWC_HOTSPOT_PASSWORD: "my-hotspot-password" # Optional. Must be 8 characters or more.
1313

1414
## Try to automatically set up a Wi-Fi network on first boot ##
15-
#PWC_SSID: "network-name" # Compulsory for this feature
16-
#USERNAME: "username" # Optional
17-
#PWC_PASSWORD: "your-password" # Optional. Must be 8 characters or more.
15+
#PWC_AC_SSID: "network-name" # Compulsory for this feature
16+
#PWC_AC_USERNAME: "username" # Optional
17+
#PWC_AC_PASSWORD: "your-password" # Optional. Must be 8 characters or more.
18+
19+
## Wi-Fi Interface ##
20+
# PWC_INTERFACE: 'wlan0' # When disabled interface is auto detected
1821

1922
## Enable/Disable LED interaction ##
2023
PWC_LED: "ON"
2124

22-
## Required System Variables ##
25+
## Required system variables ##
2326
DBUS_SYSTEM_BUS_ADDRESS: "unix:path=/host/run/dbus/system_bus_socket"
2427
build:
2528
context: .
@@ -30,4 +33,4 @@ services:
3033
io.balena.features.dbus: '1'
3134
cap_add:
3235
- NET_ADMIN
33-
privileged: true # This can be removed if you do not need the LED indicator (see the docs for more info).
36+
privileged: true # This can be removed if you do not need the LED indicator.

src/common/errors.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class WifiConnectionFailed(Exception):
2727
pass
2828

2929

30+
# Error classes for Flask-Restful
31+
class WifiDeviceNotFound(Exception):
32+
pass
33+
34+
3035
class WifiHotspotStartFailed(Exception):
3136
pass
3237

@@ -49,6 +54,10 @@ class WifiNoSuitableDevice(Exception):
4954
"message": "System error while establishing Wi-Fi connection.",
5055
"status": 500
5156
},
57+
"WifiDeviceNotFound": {
58+
"message": "Requested device not available.",
59+
"status": 500
60+
},
5261
"WifiHotspotStartFailed": {
5362
"message": "System error starting hotspot.",
5463
"status": 500

src/common/nm_dicts.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ def get_nm_dict(conn_type, ssid, username, password):
2424
}
2525

2626
# Include a key-mgmt string in hotspot if setting a password
27-
if password:
27+
if config.hotspot_password:
2828
password_key_mgmt = {'802-11-wireless-security':
29-
{'key-mgmt': 'wpa-psk', 'psk': password}}
29+
{'key-mgmt': 'wpa-psk',
30+
'psk': config.hotspot_password}}
3031

3132
hs_dict.update(password_key_mgmt)
3233

src/common/wifi.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import config
2-
import NetworkManager
2+
import NetworkManager as Pnm # Python NetworkManager
33
import socket
44
import subprocess
55
import time
66
from common.errors import logger
77
from common.errors import WifiConnectionFailed
8+
from common.errors import WifiDeviceNotFound
89
from common.errors import WifiHotspotStartFailed
910
from common.errors import WifiNetworkManagerError
1011
from common.errors import WifiNoSuitableDevice
@@ -23,8 +24,8 @@ def analyse_access_point(ap):
2324
# Based on a subset of the AP_SEC flag settings
2425
# (https://developer.gnome.org/NetworkManager/1.2/nm-dbus-types.html#NM80211ApSecurityFlags)
2526
# to determine which type of security this AP uses.
26-
AP_SEC = NetworkManager.NM_802_11_AP_SEC_NONE
27-
if ap.Flags & NetworkManager.NM_802_11_AP_FLAGS_PRIVACY and \
27+
AP_SEC = Pnm.NM_802_11_AP_SEC_NONE
28+
if ap.Flags & Pnm.NM_802_11_AP_FLAGS_PRIVACY and \
2829
ap.WpaFlags == AP_SEC and \
2930
ap.RsnFlags == AP_SEC:
3031
security = config.type_wep
@@ -36,9 +37,9 @@ def analyse_access_point(ap):
3637
security = config.type_wpa2
3738

3839
if ap.WpaFlags & \
39-
NetworkManager.NM_802_11_AP_SEC_KEY_MGMT_802_1X or \
40+
Pnm.NM_802_11_AP_SEC_KEY_MGMT_802_1X or \
4041
ap.RsnFlags & \
41-
NetworkManager.NM_802_11_AP_SEC_KEY_MGMT_802_1X:
42+
Pnm.NM_802_11_AP_SEC_KEY_MGMT_802_1X:
4243
security = config.type_enterprise
4344

4445
entry = {"ssid": ap.Ssid,
@@ -69,7 +70,7 @@ def auto_connect(ssid=None,
6970
# Returns True when a connection to a router is made, or the Hotspot is live
7071
def check_device_state():
7172
# Save the wi-fi device object to a variable
72-
if get_device().State == NetworkManager.NM_DEVICE_STATE_ACTIVATED:
73+
if get_device().State == Pnm.NM_DEVICE_STATE_ACTIVATED:
7374
return True
7475
else:
7576
return False
@@ -112,24 +113,20 @@ def connect(conn_type=config.type_hotspot,
112113
# Remove any existing connection made by this app
113114
forget()
114115

115-
# If user has specified a password for their hotspot
116-
if conn_type == config.type_hotspot and config.hotspot_password:
117-
password = config.hotspot_password
118-
119116
# Get the correct config based on type requested
120117
conn_dict = get_nm_dict(conn_type, ssid, username, password)
121118

122119
try:
123-
NetworkManager.Settings.AddConnection(conn_dict)
120+
Pnm.Settings.AddConnection(conn_dict)
124121
logger.info(f"Adding connection of type {conn_type}")
125122

126123
# Save the wi-fi device object to a variable
127124
dev = get_device()
128125

129126
# Connect
130-
NetworkManager.NetworkManager.ActivateConnection(get_connection_id(),
131-
dev,
132-
"/")
127+
Pnm.NetworkManager.ActivateConnection(get_connection_id(),
128+
dev,
129+
"/")
133130

134131
# If not a hotspot, log the connection SSID being attempted
135132
if conn_type != config.type_hotspot:
@@ -174,7 +171,7 @@ def forget(create_new_hotspot=False, all_networks=False):
174171
# Find and delete the hotspot connection
175172
try:
176173
if all_networks:
177-
for connection in NetworkManager.Settings.ListConnections():
174+
for connection in Pnm.Settings.ListConnections():
178175
if connection.GetSettings()["connection"]["type"] \
179176
== "802-11-wireless":
180177
# Delete the identified connection
@@ -183,6 +180,8 @@ def forget(create_new_hotspot=False, all_networks=False):
183180
logger.debug(f"Deleted connection: {network_id}")
184181
else:
185182
connection_id = get_connection_id()
183+
# connection_id returns false if it is missing. This can be ignored
184+
# as this function is often called as a precautionary clean up
186185
if connection_id:
187186
connection_id.Delete()
188187
logger.debug(f"Deleted connection: {config.ap_name}")
@@ -204,7 +203,7 @@ def forget(create_new_hotspot=False, all_networks=False):
204203

205204
def get_connection_id():
206205
connection = dict([(x.GetSettings()['connection']['id'], x)
207-
for x in NetworkManager.Settings.ListConnections()])
206+
for x in Pnm.Settings.ListConnections()])
208207

209208
if config.ap_name in connection:
210209
return connection[config.ap_name]
@@ -213,11 +212,26 @@ def get_connection_id():
213212

214213

215214
def get_device():
215+
# Configured interface variable takes precedent.
216+
if config.interface:
217+
logger.debug(f"Interface {config.interface} selected.")
218+
for device in Pnm.NetworkManager.GetDevices():
219+
if device.DeviceType != Pnm.NM_DEVICE_TYPE_WIFI:
220+
continue
221+
# For each Wi-Fi network interface, check the interface name
222+
# against the one configured in config.interface
223+
if (device.Udi[device.Udi.rfind('/')+1:].lower()
224+
== config.interface.lower()):
225+
return device
226+
else:
227+
raise WifiDeviceNotFound
228+
229+
# Fetch last Wi-Fi interface found
216230
devices = dict([(x.DeviceType, x)
217-
for x in NetworkManager.NetworkManager.GetDevices()])
231+
for x in Pnm.NetworkManager.GetDevices()])
218232

219-
if NetworkManager.NM_DEVICE_TYPE_WIFI in devices:
220-
return devices[NetworkManager.NM_DEVICE_TYPE_WIFI]
233+
if Pnm.NM_DEVICE_TYPE_WIFI in devices:
234+
return devices[Pnm.NM_DEVICE_TYPE_WIFI]
221235
else:
222236
logger.error("No suitable or available device found.")
223237
raise WifiNoSuitableDevice

src/config.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23

34
# Set default Wi-Fi SSID.
@@ -25,17 +26,24 @@
2526
port = 9090
2627

2728
# Compile kwargs for automatic connection
28-
if "PWC_SSID" in os.environ:
29+
if "PWC_AC_SSID" in os.environ:
2930
auto_connect_kargs = \
30-
{"ssid": os.environ['PWC_SSID']}
31+
{"ssid": os.environ['PWC_AC_SSID']}
3132

32-
if "PWC_USERNAME" in os.environ:
33-
auto_connect_kargs.update(username=os.environ['PWC_USERNAME'])
34-
if "PWC_PASSWORD" in os.environ:
35-
auto_connect_kargs.update(password=os.environ['PWC_PASSWORD'])
33+
if "PWC_AC_USERNAME" in os.environ:
34+
auto_connect_kargs.update(username=os.environ['PWC_AC_USERNAME'])
35+
if "PWC_AC_PASSWORD" in os.environ:
36+
auto_connect_kargs.update(password=os.environ['PWC_AC_PASSWORD'])
3637
else:
3738
auto_connect_kargs = False
3839

40+
# Set default interface
41+
if "PWC_INTERFACE" in os.environ and \
42+
json.loads(os.environ['PWC_INTERFACE']) is not False:
43+
interface = os.environ['PWC_INTERFACE']
44+
else:
45+
interface = False
46+
3947
# Default access point name. No need to change these under usual operation as
4048
# they are for use inside the app only. PWC is acronym for 'Py Wi-Fi Connect'.
4149
ap_name = 'PWC'

src/resources/wifi_routes.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import config
12
import threading
23
from common.errors import logger
34
from common.wifi import check_internet_status
@@ -49,21 +50,20 @@ def post(self):
4950
if not check_wifi_status():
5051
return {'message': 'Device is already disconnected.'}, 409
5152

52-
# Check the all_networks boolean is valid
53+
# Check the all_networks boolean
5354
if (not request.get_json() or
54-
'all_networks' not in request.get_json()
55-
or type(request.get_json()['all_networks']) is not bool):
56-
return {'message': "all_networks boolean missing or is not "
57-
"a boolean."}, 202
55+
'all_networks' not in request.get_json()):
56+
forget_mode = False
57+
else:
58+
forget_mode = request.get_json()['all_networks']
5859

5960
# Use threading so the response can be returned before the user is
6061
# disconnected.
6162
wifi_forget_thread = threading.Thread(target=forget,
6263
kwargs={'create_new_hotspot':
6364
True,
6465
'all_networks':
65-
request.get_json()
66-
['all_networks']})
66+
forget_mode})
6767

6868
logger.info('Removing connetion...')
6969
wifi_forget_thread.start()
@@ -76,3 +76,15 @@ def get(self):
7676
ssids, iw_status = list_access_points()
7777

7878
return {'ssids': ssids, 'iw_compatible': iw_status}
79+
80+
81+
class wifi_set_interface(Resource):
82+
def post(self):
83+
# Check entry exists
84+
if (not request.get_json() or
85+
'interface' not in request.get_json()):
86+
return {'message': 'Interface value not provided.'}, 500
87+
else:
88+
config.interface = request.get_json()['interface']
89+
logger.info(f"Interface changed to {config.interface}")
90+
return {'message': 'ok'}, 200

src/run.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from resources.wifi_routes import wifi_connection_status
1919
from resources.wifi_routes import wifi_forget
2020
from resources.wifi_routes import wifi_list_access_points
21+
from resources.wifi_routes import wifi_set_interface
2122
from waitress import serve
2223

2324

@@ -35,6 +36,7 @@
3536
api.add_resource(wifi_connection_status, '/v1/connection_status')
3637
api.add_resource(wifi_forget, '/v1/forget')
3738
api.add_resource(wifi_list_access_points, '/v1/list_access_points')
39+
api.add_resource(wifi_set_interface, '/v1/set_interface')
3840

3941
if __name__ == '__main__':
4042
# Begin loading program
@@ -46,6 +48,10 @@
4648
# Allow time for an exsiting saved Wi-Fi connection to connect.
4749
time.sleep(10)
4850

51+
# Log interface status
52+
if config.interface:
53+
logger.info(f"Interface set to {config.interface}")
54+
4955
# If the Wi-Fi connection or device is already active, do nothing
5056
if check_wifi_status() or check_device_state():
5157
led(1)

0 commit comments

Comments
 (0)