Skip to content

Instantly share code, notes, and snippets.

@ilovetogetspamed
Created September 7, 2018 13:22
Show Gist options
  • Save ilovetogetspamed/242746c5d46eb2fc8539fa3f75137e5b to your computer and use it in GitHub Desktop.
Save ilovetogetspamed/242746c5d46eb2fc8539fa3f75137e5b to your computer and use it in GitHub Desktop.
How to work with DataView objects from objects higher up in the widget chain.
from kivy.base import Builder
from kivy.uix.screenmanager import Screen
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.logger import Logger
from kivy.uix.accordion import Accordion, AccordionItem
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview import RecycleDataModel
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.gridlayout import GridLayout
from kivy.properties import BooleanProperty
from kivy.properties import ObjectProperty
from kivy.clock import Clock
from kivy.network.urlrequest import UrlRequest
from time import sleep
import re
import socket
import ipaddress
import json
import os
from jinja2 import Template
from subprocess import check_output, call, CalledProcessError
import StringIO
from kivy.core.text import LabelBase
LabelBase.register(name='FontAwesome',
fn_regular='../assets/fonts/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf')
LabelBase.register(name='LCDMono',
fn_regular='../assets/fonts/LCDMonoWinTT/LCDM2N__.TTF',
fn_bold='../assets/fonts/LCDMonoWinTT/LCDM2B__.TTF')
# todo DRY this up.
UNLOCK_GLYPH = u'\uF09C'
LOCK_GLYPH = u'\uF023'
kv = """
<WiFi_DataView>:
signal_level: ''
ssid: ''
protected: ''
# # Draw a background to indicate selection
canvas.before:
Color:
# rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
rgba: (.0, 0.9, .1, .3) if self.selected else (0.4, 0.4, 0.4, 1)
Rectangle:
pos: self.pos
size: self.size
Line:
rectangle: self.x, self.y, self.width, self.height
padding: dp(4)
rows:1
cols: 3
Label:
id: signal_level_label
text: root.signal_level
Label:
id: ssid_label
text: root.ssid
Label:
id: protected_label
text: root.protected
font_name: "FontAwesome"
markup: True
<WiFi_RecycleView>:
viewclass: 'WiFi_DataView'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: False
<WiFiNetworkAccordionItem>:
id: wifi_accordion_item
min_space:
collapse: False
title: 'WiFi Network Configuration'
GridLayout:
padding: dp(10)
spacing: dp(5)
rows: 4
canvas:
Color:
rgba: 0.4, 0.4, 0.4, 1
Rectangle:
size: self.size
pos: self.pos
row_default_height: 40
row_force_default: True
GridLayout:
cols: 1
Button:
background_normal: ''
border: [16, 16, 16, 16]
text: "Search for WiFi networks"
font_size: 22
color: .5,1,.5,1
on_press: root.scan_wifi_networks()
GridLayout:
rows: 1
cols: 2
Label:
text: "SSID"
size_hint_x: None
width: 100
TextInput:
id: wifi_ssid
multiline: False
hint_text: 'Your ESSID'
config_key: {'section': "network", 'option': "wifi_ssid"}
on_text_validate: root.write_to_configuration(self)
GridLayout:
rows: 1
cols: 2
Label:
text: "PSK"
size_hint_x: None
width: 100
TextInput:
id: wifi_psk
multiline: False
password: True
hint_text: 'Your Wifi password'
config_key: {'section': "network", 'option': "wifi_psk"}
on_text_validate: root.write_to_configuration(self)
GridLayout:
rows: 1
cols: 1
row_default_height: 700
WiFi_RecycleView:
id: rv
<Test>:
Accordion:
orientation: 'horizontal'
WiFiNetworkAccordionItem:
id: wifi_network_configuration
"""
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
""" Adds selection and focus behaviour to the view. """
keyboard_mode = 'managed' # to disable keyboard popup on row selection.
class WiFi_DataView(RecycleDataViewBehavior, GridLayout):
''' Add selection support to the GridLayout '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
missing = BooleanProperty(False)
def __init__(self, **kwargs):
super(WiFi_DataView, self).__init__(**kwargs)
self.app = App.get_running_app()
def refresh_view_attrs(self, rv, index, data):
""" Catch and handle the view changes """
self.index = index
return super(WiFi_DataView, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
''' Add selection on touch down '''
if super(WiFi_DataView, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos) and self.selectable:
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
''' Respond to the selection of items in the view. '''
self.selected = is_selected # setting this turns on the row highlighting (see kv file)
if is_selected:
# update the SSID TextInput text.
self.parent.parent.parent.parent.children[2].children[0].text = self.ids.ssid_label.text
# set focus to the PSK TextInput widget.
self.parent.parent.parent.parent.children[1].children[0].focus = True
class WiFi_SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
""" Adds selection and focus behaviour to the view. """
keyboard_mode = 'managed' # to disable keyboard popup on row selection.
class WiFi_RecycleView(RecycleView):
""" SSID RecycleView """
def __init__(self, **kwargs):
super(WiFi_RecycleView, self).__init__(**kwargs)
class WiFiNetworkAccordionItem(AccordionItem):
def __init__(self, **kwargs):
super(WiFiNetworkAccordionItem, self).__init__(**kwargs)
self.app = App.get_running_app()
def scan_wifi_networks(self):
Logger.info('Configuration Screen: Finding WiFi networks.')
# Scan the WiFi network using built-in CLI tool
cells = []
wifi_networks = check_output("sudo wifi scan", shell=True)
for network in StringIO.StringIO(wifi_networks):
cells.append(network.split())
# Load data into GridView
self.ids.rv.data = [] # clear list view data
# update RecycleView values with new data.
if cells:
try:
self.ids.rv.data = [
{'signal_level': cell[0],
'ssid': cell[1],
'protected': LOCK_GLYPH if cell[2] == 'protected' else UNLOCK_GLYPH
} for cell in cells]
except IndexError:
print 'Index Error Trapped! wifi scan returned bad data... cleaning up.'
self.ids.rv.data = [] # clear list view data
for cell in cells:
# print len(cell), cell
if len(cell) == 3:
self.ids.rv.data.append(
{
'signal_level': cell[0],
'ssid': cell[1],
'protected': LOCK_GLYPH if cell[2] == 'protected' else UNLOCK_GLYPH
}
)
continue
elif len(cell) == 4:
self.ids.rv.data.append(
{
'signal_level': cell[0],
'ssid': cell[1] + cell[2],
'protected': LOCK_GLYPH if cell[3] == 'protected' else UNLOCK_GLYPH
}
)
continue
elif len(cell) == 2:
self.ids.rv.data.append(
{
'signal_level': cell[0],
'ssid': 'Hidden SSID',
'protected': LOCK_GLYPH if cell[1] == 'protected' else UNLOCK_GLYPH
}
)
else:
print 'bad data: ' + cell.__str__()
def textinput_on_focus(self, instance, keyboard):
if keyboard and keyboard.widget:
# keyboard.widget.scale = 0.9
if keyboard.widget.top >= instance.get_top() - instance.height:
keyboard.widget.top = instance.get_top()\
+ keyboard.widget.height\
+ instance.height * 1.5
# keyboard.widget.layout = './assets/keyboards/numeric.json'
def write_to_configuration(self, instance):
"""
write_to_configuration
:param instance: The control that is sending the write_to_configuration command.
:return: True or False depending on the acceptance of the instance.text
"""
Logger.info('Configuration Screen: write_to_configuration instance.text: {}'.format(instance.text))
if instance.text == '':
return False
else:
# self.validate(instance.config_key, instance.text)
try:
self.app.config.get(instance.config_key['section'], instance.config_key['option'])
except Exception as e:
Logger.error('Configuration Screen: WifiNetworkAccordionItem Error: {}'.format(e.message))
return False
if self.validate(instance.config_key, instance.text):
self.app.config.set(instance.config_key['section'],
instance.config_key['option'], instance.text)
self.app.config.write()
Logger.info('Configuration Screen: WifiNetworkAccordionItem: Wrote {} to [{}] {}'.format(
instance.text, instance.config_key['section'], instance.config_key['option']))
# try to start WiFi
if instance.config_key['section'] == 'network' and instance.config_key['option'] == 'wifi_psk':
# Update wpa_supplicant/wpa_supplicant.conf
cmd = "wpa_passphrase {} {}".format(self.ids.wifi_ssid.text, self.ids.wifi_psk.text)
try:
cmd_result = check_output(cmd, shell=True)
new_ssid = str(cmd_result[cmd_result.find('ssid'):cmd_result.find('\n\t#psk')]).split('=')[1]
new_ssid = new_ssid.replace('"', '')
new_psk = cmd_result[cmd_result.rfind('psk'):cmd_result.rfind('\n}\n')].split('=')[1]
# print(new_ssid, new_psk) # debug
except CalledProcessError as e:
Logger.error('Configuration Screen: WifiNetworkAccordionItem Error: 821 {}'.format(e)) # todo popup?
return False
wpaTemplate = """
network={
ssid="WIFI-SSID"
proto=RSN
scan_ssid=1
psk="WIFI-PSK"
key_mgmt=WPA-PSK
pairwise=CCMP
auth_alg=OPEN
priority=1
}
"""
# see "man wpa_supplicant.conf" for valid settings.
wifiText = wpaTemplate\
.replace("WIFI-SSID", new_ssid)\
.replace("WIFI-PSK", new_psk)
# print(wifiText) # debug
try:
# to make is simple we just append the new wifi settings.
with open("my_wpa_supplicant.conf", "w") as wifiFile:
wifiFile.write(wifiText)
except IOError:
Logger.error('Configuration Screen: WifiNetworkAccordionItem: Unable to write WiFi config')
# todo What's the best way to update the OS? I'm thinking about Ansible.
# try:
# # update the configuration
# call("sudo wpa_cli reconfigure", shell=True)
# # todo schedule popup with success or failure
# except OSError as msg:
# print(msg)
# Logger.error('Configuration Screen: WifiNetworkAccordionItem: Unable to restart WiFi')
return True
else:
Logger.warning('Configuration Screen: WifiNetworkAccordionItem: Invalid value {} for [{}] {}'.format(
instance.text, instance.config_key['section'], instance.config_key['option']))
instance.focus = True
popup = Popup(title='Invalid Data (734)', title_color=[1, 0, 0, 1], # color in the Kivy format (r, g, b, a).
seperator_color=[255, 0, 0, 1], seperator_height=15,
content=Label(text='Please try again.',
halign='center', valign='middle'),
size_hint=(None, None), size=(300, 300))
popup.open()
# dismiss the popup after 1.25 seconds.
Clock.schedule_once(lambda *x: popup.dismiss(), 1.25)
# remove invalid data from field:
while instance._undo:
instance.do_undo()
instance.focus = True
return False
def validate(self, config_key, value):
Logger.info(
"Configuration Screen: WifiNetworkAccordionItem: validate got: config_key: {} value: {}".format(
config_key, value))
try:
''' validators must return True of False '''
validator = config_key['section'] + '_' + config_key['option']
return validators[validator](value)
except Exception as e:
Logger.warn(
'Configuration Screen: WifiNetworkAccordionItem: Validator Exception: {}'.format(e.message))
return False
class Test(BoxLayout):
def __init__(self, *args, **kwargs):
super(Test, self).__init__(*args, **kwargs)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
Builder.load_string(kv)
TestApp().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment