AppleScript IOBluetooth issue, but only when exported as an app - bluetooth

Based on https://stackoverflow.com/a/57776820/2654603 I am using the IOBluetooth framework to detect disconnect/reconnect of my keyboard, such that switching it to a second host triggers changing the display input to that host as well.
When I run it in Script Editor it detects both connected and disconnected states as I switch back and forth. When I export it as an App (with Run-only) and run it, it starts out detecting the correct state. However once the keyboard has disconnected and later reconnects it never detects the connected state again.
If it matters, I'm on Catalina (10.15.7) on a 2019 MacBook Pro.
use framework "IOBluetooth"
use scripting additions
property lastStatus : true
set debug to true
set myKeyboard to "Keychron"
repeat
set kbStatus to isDeviceConnected(myKeyboard)
if kbStatus is not equal to lastStatus then
if debug is false then
if kbStatus is true then
do shell script "/usr/local/bin/ddcctl -d 1 -i 27"
else
do shell script "/usr/local/bin/ddcctl -d 1 -i 17"
end if
else
log "Status changed to " & kbStatus
end if
set lastStatus to kbStatus
end if
delay 1
end repeat
on isDeviceConnected(substring)
repeat with device in (current application's IOBluetoothDevice's pairedDevices() as list)
if (device's nameOrAddress as string) contains substring then
if device's isConnected then
return true
else
return false
end if
end if
end repeat
return false
end isDeviceConnected
Edit: I have inserted lots of debug log messages in various places. When it fails to detect the reconnect, it is still matching my keyboard name in the pairedDevices() list, just not as connected.

I suspect the problem is that you didn't add parentheses to some of your objC methods, specifically: nameOrAddress() and isConnected(). I'll add that it's far better to use an idle loop than an endless repeat loop. That would look like this:
property lastStatus : true
property debug : true
property myKeyboard : "Keychron"
on idle
set kbStatus to isDeviceConnected(myKeyboard)
if kbStatus is not equal to lastStatus then
if debug is false then
if kbStatus is true then
do shell script "/usr/local/bin/ddcctl -d 1 -i 27"
else
do shell script "/usr/local/bin/ddcctl -d 1 -i 17"
end if
else
log "Status changed to " & kbStatus
end if
set lastStatus to kbStatus
end if
return 1
end idle
But set all that aside, because I think the best solution is to use the IOBluetoothDevice methods for registering observers. That way your app will sleep quietly in the background until it gets a notification that a device has been connected or disconnected. Copy the following script into Script Editor:
use AppleScript version "2.4" -- Yosemite 10.10 or later
use framework "IOBluetooth"
use scripting additions
property IOBluetoothDevice : class "IOBluetoothDevice"
property myKeyboard : "Keychron"
property connectionNotifObj : missing value
property disconnectionNotifObj : missing value
on run
try
set connectionNotifObj to IOBluetoothDevice's registerForConnectNotifications:me selector:"didConnectNotif:forDevice:"
on error errstr
display dialog errstr
end try
end run
on quit
try
connectionNotifObj's unregister()
end try
set connectionNotifObj to missing value
continue quit
end quit
on didConnectNotif:notif forDevice:dev
if (dev's nameOrAddress() as text) contains myKeyboard then
set disconnectionNotifObj to (dev's registerForDisconnectNotification:me selector:"didDisconnectNotif:forDevice:")
end if
end didConnectNotif:forDevice:
on didDisconnectNotif:notif forDevice:dev
if (dev's nameOrAddress() as string) contains myKeyboard then
try
disconnectionNotifObj's unregister()
end try
set disconnectionNotifObj to missing value
end if
end didDisconnectNotif:forDevice:
Save the script as an application — make sure you click the stay open after run handler checkbox so the script app doesn't auto-quit — and you should be good to go.
If you want the script app to be invisible to the Dock and App Picker, run the following command in terminal:
defaults write '/path/to/$name.app/Contents/Info.plist' LSUIElement -bool yes
Of course, that makes quitting it manually a bit more of a headache (you'll have to use Terminal or Activity Monitor to see it running), but it's visually more pleasing.

Related

How to find whether a certain bluetooth device is connected?

I want to use applescript to do a periodic (every second) check to see if a specific bluetooth devices is connected, and if so, to flash up a quick notification. To frame it, I want a popup when my Airpods connect, since sometimes when I pull them out, the connect to my computer, and sometimes to my iPhone.
I've got everything figured out, except for the bluetooth check part. I've used this as a starting point, but can't get it to work. Any help would be appreciated.
repeat
set statusOld to checkStatus()
set statusNew to checkStatus()
repeat while statusOld is equal to statusNew
delay 1 --for 1 second checks
set statusNew to checkStatus()
end repeat
if statusNew is true then
display dialog "Device Added - put some real code here"
else
display dialog "Device Removed - put some real code here"
end if
end repeat
on checkStatus()
(*Delete the 2 lines below when done testing*)
--set myString to button returned of (display dialog "Connected?" buttons {"Yes", "No"})
--set myString to "name: DR-BT101 Connected: " & myString
(*uncomment line below when done testing*)
set myString to do shell script "system_profiler SPBluetoothDataTyp"
--initial check if it's not even there
if myString does not contain "Christian’s AirPods" then
return false
else
--find out if connected/disconnected
set AppleScript's text item delimiters to "name:"
set myList to the text items of myString --each item of mylist is now one of the devices
set numberOfDevices to count of myList
set counter to 1
repeat numberOfDevices times --loop through each devices checking for Connected string
if item counter of myList contains "Christian’s AirPods" then
if item counter of myList contains "Connected: Yes" then
return true
else if item counter of myList contains "Connected: No" then
return false
else
display dialog "Error Parsing" --this shouldn't happen
end if
end if
set counter to counter + 1
end repeat
end if
end checkStatus
You're missing the e:
set myString to do shell script "system_profiler SPBluetoothDataType"
^
I'm working on something similar. This seems to work well on macOS Mojave:
use framework "IOBluetooth"
use scripting additions -- https://stackoverflow.com/a/52806598/6962
on isDeviceConnected(substring)
repeat with device in (current application's IOBluetoothDevice's pairedDevices() as list)
if device's isConnected and (device's nameOrAddress as string) contains substring then return true
end repeat
return false
end isDeviceConnected
-- Usage example:
isDeviceConnected("AirPods")
I combined it with a launch agent like this: https://gist.github.com/henrik/3d4c622a5567cdf2bf461352f48ad4dd

Determine if Window is Open Using UIAutomation

I am writing an excel plugin that outputs an edge list and a graphml sheet that are later used by the program yED to make a bitmap of the graph for the overall output of the plugin. I am using a shell command to open the appropriate file in yED, and UIAutomation to send the commands to yED.
When there is a window of yED already open, the code executes just fine. The window is found by the polling, then set active and the commands sent over. Where it goes wrong is when the shell command causes a new window of yED to be launched. There is a splash screen for yED as it is loading in that takes a few seconds to get through, and shares the same Name and Class as the window that I am looking for. The HWND is different between the two.
My code will error out whenever there is a new window of yED launching. The error reads:
Run-time error '-2147467259 (80004005)': Automation error Unspecified
error
Reference code:
Function FindyEdByClass() As IUIAutomationElement
Dim oUIAutomation As New CUIAutomation
Dim oUIADesktop As IUIAutomationElement
Dim allChilds As IUIAutomationElementArray
Dim oUIAyED As IUIAutomationElement
Dim i As Integer
Dim Timer As Date
Set oUIADesktop = oUIAutomation.GetRootElement
Set oUIAyED = oUIADesktop
Timer = Now
RestartLoop:
Set allChilds = oUIADesktop.FindAll(TreeScope_Children, oUIAutomation.CreateTrueCondition)
Debug.Print "StartLoop" & vbCrLf;
For i = 0 To allChilds.Length - 1
'EDIT: the following line is the one that errors out.
If allChilds.GetElement(i).CurrentName = "Graph.graphml - yEd" And allChilds.GetElement(i).CurrentClassName = "SunAwtFrame" Then
Debug.Print "Found Child - yED" & vbCrLf;
Set oUIAyED = allChilds.GetElement(i)
End If
Next
If Now() > (Timer + TimeValue("00:00:10")) Then GoTo NoyED
If oUIAyED.CurrentName = "Desktop" Then GoTo RestartLoop
EndOFLoop:
Debug.Print oUIAyED.CurrentName & " " & oUIAyED.CurrentClassName & vbCrLf;
Set FindyEdByClass = oUIAyED
Exit Function
NoyED:
MsgBox "No yED Window Found"
End
End Function
The line it errors out on the If statement. Through use of Debug.Print and FindWindowEx I have discovered that the object it errors out on is not the yED window I am looking for, but the yED splash screen that precedes it. I assume that the error is caused by the splash screen disappearing after its load time is up.
How do I go about finding the window I am looking for in this case? I need to be able to find the window without seeing the splash screen that will cause the error, and I need to be able to differentiate between the two by something other than class or name.
Note: I want to compare the windows by HWND but I don't know how to find it without the class/name, and it is never the same between two runs.

HPQC ALM - How to make TC_PLAN_SCHEDULING_DATE Read Only

I am trying to make "Planned Exec Date" field read only inTest Lab --> Execution Grid.
In Test Lab Module script --> TesSetTests_FieldCanChange Sub, I have added the following code:
TestSetTest_Fields.Field("TC_PLAN_SCHEDULING_DATE").IsReadOnly = True
But still date can be changed and added by writing in the field or choosing from calendar.
How do I make this field read only?
Thanks.
Sohel
I was able to resolve the problem by putting the code into TestSetTests_MoveTo sub.
Sub TestSetTests_MoveTo
On Error Resume Next
TestSetTest_Fields.Field("TC_PLAN_SCHEDULING_DATE").IsReadOnly = True
End Sub

Display and update applescript output in background

I have a short and sweet program that outputs my internal and external ip in applescript.
Here it is the Applescript code:
set inIP to IPv4 address of (get system info)
set exIP to (do shell script "curl ipecho.net/plain")
display dialog "Internal: " & inIP & "
External: " & exIP
I would like it to constantly update in the background and preferably not in a display dialog function as it does at the moment.
I do not want a display dialog constantly popping up so I am looking for example, displaying the IPs in the menu bar.
I do not know if this is possible to do with Applescript
As from 10.10 (i Think) you can create real application using ApplescriptOBJC directly in Script Editor.
I have not really tried it before but once you get going it is easier than I expected.
Paste this code in a new Script Editor Applescript document.
Save it as a Stay open Application using the Save as… menu option.
Then run the app as a normal application.
Using the OP's original applescript code
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
property StatusItem : missing value
-- check we are running in foreground - YOU MUST RUN AS APPLICATION. to be thread safe and not crash
if not (current application's NSThread's isMainThread()) as boolean then
display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
error number -128
end if
-- create an NSStatusBar
on makeStatusBar()
set bar to current application's NSStatusBar's systemStatusBar
set StatusItem to bar's statusItemWithLength:-1.0
-- set up the initial NSStatusBars title
StatusItem's setTitle:"IP"
end makeStatusBar
-- update statusBar
on displayIP(theDisplay)
StatusItem's setTitle:theDisplay
end displayIP
--repeat run update code
on idle
--get the IPs
set inIP to IPv4 address of (get system info)
set exIP to (do shell script "curl ipecho.net/plain")
set theDisplay to "Internal: " & inIP & " External: " & exIP
my displayIP(theDisplay)
return 30 -- run every 30 seconds
end idle
-- call to create initial NSStatusBar
my makeStatusBar()
The app is set to run every 30 seconds.
It will update a status bar menu in the menu bar with your ips.
I have not put any error checking in and leave that to you.
Also remember if you want to run the code while in Script Editor then make sure you use "Run Application".
Update:1
I have changed the internal IP address code to use NShost which is quicker and probably more reliable than the "get system info"
Update:2
Update the external code to use a NSURL request rather than the Original Curl do shell script command.
This allows for easier error checks if the is a failure in obtaining the external ip address due to no network connection...etc.
Curl will return a whole log of info as to why it failed and be IMHO a pain.
Updated applescript code
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
property StatusItem : missing value
-- check we are running in foreground - YOU MUST RUN AS APPLICATION. to be thread safe and not crash
if not (current application's NSThread's isMainThread()) as boolean then
display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
error number -128
end if
-- create an NSStatusBar
on makeStatusBar()
set bar to current application's NSStatusBar's systemStatusBar
set StatusItem to bar's statusItemWithLength:-1.0
-- set up the initial NSStatusBars title
StatusItem's setTitle:"IP"
end makeStatusBar
-- update statusBar
on displayIP(theDisplay)
StatusItem's setTitle:theDisplay
end displayIP
--repeat run update code
on idle
--get the IPs
set stringAddress to ""
--use NSHost to get the Internal IP address
set inIPAddresses to current application's NSHost's currentHost's addresses
--work through each item to find the IP
repeat with i from 1 to number of items in inIPAddresses
set anAddress to (current application's NSString's stringWithString:(item i of inIPAddresses))
set ipCheck to (anAddress's componentsSeparatedByString:".")
set the Counter to (count of ipCheck)
if (anAddress as string) does not start with "127" then
if Counter is equal to 4 then
set stringAddress to anAddress
-- found a match lets exit the repeat
exit repeat
end if
else
set stringAddress to "Not available"
end if
end repeat
-- Get extenal IP
set anError to missing value
set iPURL to (current application's NSURL's URLWithString:"http://ipecho.net/plain")
set NSUTF8StringEncoding to 4
set exIP to (current application's NSString's stringWithContentsOfURL:iPURL encoding:NSUTF8StringEncoding |error|:anError) as string
if exIP contains missing value then
set exIP to "Not available"
end if
set theDisplay to "Intl: " & stringAddress & " Extnl: " & exIP
--call to update statusBar
my displayIP(theDisplay)
return 30 -- run every 30 seconds
end idle
-- call to create initial NSStatusBar
my makeStatusBar()
UPDATE 3
This one will do as the OP asked in the comments.
It now has a drop down menu with two options External or Internal.
Select one or the other menu item will change the status bar to show the chosen IP.
This last one was thrown together quickly so it is not pretty. :-)
( UPDATE 4 It also persists the selection on quitting the app and relaunching. )
New code:
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"
property StatusItem : missing value
property selectedMenu : "" -- each menu action will set this to a number, this will determin which IP is shown
property theDisplay : ""
property defaults : class "NSUserDefaults"
-- check we are running in foreground - YOU MUST RUN AS APPLICATION. to be thread safe and not crash
if not (current application's NSThread's isMainThread()) as boolean then
display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
error number -128
end if
-- create an NSStatusBar
on makeStatusBar()
set bar to current application's NSStatusBar's systemStatusBar
set StatusItem to bar's statusItemWithLength:-1.0
-- set up the initial NSStatusBars title
StatusItem's setTitle:"IP"
set newMenu to current application's NSMenu's alloc()'s initWithTitle:"Custom"
set internalMenuItem to current application's NSMenuItem's alloc()'s initWithTitle:"Internal" action:"showInternal:" keyEquivalent:""
set externalMenuItem to current application's NSMenuItem's alloc()'s initWithTitle:"External" action:"showIExternal:" keyEquivalent:""
StatusItem's setMenu:newMenu
newMenu's addItem:internalMenuItem
newMenu's addItem:externalMenuItem
internalMenuItem's setTarget:me
externalMenuItem's setTarget:me
end makeStatusBar
--Show Internal ip Action
on showInternal:sender
defaults's setObject:"1" forKey:"selectedMenu"
my runTheCode()
end showInternal:
--Show External ip Action
on showIExternal:sender
defaults's setObject:"2" forKey:"selectedMenu"
my runTheCode()
end showIExternal:
-- update statusBar
on displayIP(theDisplay)
StatusItem's setTitle:theDisplay
end displayIP
on runTheCode()
set stringAddress to ""
--use NSHost to get the Internal IP address
set inIPAddresses to current application's NSHost's currentHost's addresses
--work through each item to find the IP
repeat with i from 1 to number of items in inIPAddresses
set anAddress to (current application's NSString's stringWithString:(item i of inIPAddresses))
set ipCheck to (anAddress's componentsSeparatedByString:".")
set the Counter to (count of ipCheck)
if (anAddress as string) does not start with "127" then
if Counter is equal to 4 then
set stringAddress to anAddress
-- found a match lets exit the repeat
exit repeat
end if
else
set stringAddress to "Not available"
end if
end repeat
-- Get extenal IP
set anError to missing value
set iPURL to (current application's NSURL's URLWithString:"http://ipecho.net/plain")
set NSUTF8StringEncoding to 4
set exIP to (current application's NSString's stringWithContentsOfURL:iPURL encoding:NSUTF8StringEncoding |error|:anError) as string
if exIP contains missing value then
set exIP to "Not available"
end if
set selectedMenu to (defaults's stringForKey:"selectedMenu") as string
if selectedMenu is "" or selectedMenu contains missing value then
set selectedMenu to "1"
end if
if selectedMenu is "1" then
set theDisplay to "Intl: " & stringAddress
else if selectedMenu is "2" then
set theDisplay to " Extnl: " & exIP
end if
--call to update statusBar
my displayIP(theDisplay)
end runTheCode
--repeat run update code
on idle
my runTheCode()
--my displayIP(theDisplay)
return 30 -- run every 30 seconds
end idle
-- call to create initial NSStatusBar
set defaults to current application's NSUserDefaults's standardUserDefaults
my makeStatusBar()

Applescript if else statement inside of an if else statement with several repeats

HI I am having trouble getting this code to work properly. Here is the code:
property firstRow : 2051
property lastRow : 5584
set r to firstRow
-- highly recommended
-- close all of Safari's windows before...
tell application "Safari"
close windows
end tell
-- loop through the given row numbers
repeat until r is (lastRow + 1)
-- get the search value for Safari auto completion (column J)
try
tell application "Microsoft Excel"
tell active sheet
set searchTerm to string value of range ("J" & r) of active sheet
end tell
end tell
on error
-- no document open, exit the script
return
end try
-- open Safari and make a new window
tell application "Safari"
activate
make new document with properties {URL:""}
delay 0.5
set pageLoaded to false
end tell
-- type the search value into the address field and hit return (aka select and open the first proposal)
tell application "System Events"
-- here with Safari 6.1 text field 1 of group 2 of tool bar 1 of window 1 points to the URL field
set focused of text field 1 of group 2 of toolbar 1 of window 1 of process "Safari" to true
delay 0.5
keystroke searchTerm
delay 1.5
keystroke return
end tell
-- let open Safari the suggested web page and read out the finally used URL
tell application "Safari"
repeat while not pageLoaded -- keep doing this loop until loading complete
delay 5
if (do JavaScript "document.readyState" in document 1) is "complete" then
set pageLoaded to true
else
-- not sure if this second else is needed, why not just wait until the first access has finished...
-- close document 1
-- make new document with properties {URL:""}
-- tell application "System Events"
-- delay 1.5
-- set focused of text field 1 of group 2 of tool bar 1 of window 1 of process "Safari" to true
-- delay 1.5
-- keystroke searchTerm
-- delay 1.5
-- keystroke return
-- end tell
end if
set thisULR to "NO SITE"
end repeat
try
set thisURL to URL of document 1
on error
set thisURL to "NO SITE"
end try
close document 1
end tell
-- write the result into the cell next to the key word (column K)
tell application "Microsoft Excel"
if thisURL ≠ "NO SITE" then
tell active sheet
make new hyperlink of cell ("K" & r) with properties {address:thisURL, name:thisURL}
end tell
else
tell active sheet
make new cell ("K" & r) with properties {name:"NO SITE"}
end tell
end if
end tell
set r to r + 1
end repeat
I am having trouble getting the code to not crash if there is no URL saved as variable thisURL.
However, it is still crashing. It often says thisURL is not defined and then it stops the script from going to the next r value instead of adding "NO SITE" to the cell. Not sure why its not working.
It was a big chaos with all your end telland end ifetc. Another thing is that I don't understand why you need the second nested repeat-loop...
But I think I figured it out: You want to
read a value from an excel sheet
pretend to type the read out value into Safari's address bar
use the autofill and read out the URL of the found site
write the result into the adjacent excel cell
After your post edit I edited the code to this:
property firstRow : 62
property lastRow : 5584
set r to firstRow
-- highly recommended
-- close all of Safari's windows before...
tell application "Safari"
close windows
end tell
-- loop through the given row numbers
repeat until r is (lastRow + 1)
-- get the search value for Safari auto completion (column J)
try
tell application "Microsoft Excel"
tell active sheet
set searchTerm to string value of range ("J" & r) of active sheet
end tell
end tell
on error
-- no document open, exit the script
return
end try
-- open Safari and make a new window
tell application "Safari"
activate
make new document with properties {URL:""}
set pageLoaded to false
end tell
-- type the search value into the address field and hit return (aka select and open the first proposal)
tell application "System Events"
-- here with Safari 6.1 text field 1 of group 2 of tool bar 1 of window 1 points to the URL field
set focused of text field 1 of group 2 of tool bar 1 of window 1 of process "Safari" to true
keystroke searchTerm
delay 1.5
keystroke return
end tell
-- let open Safari the suggested web page and read out the finally used URL
tell application "Safari"
try
repeat while not pageLoaded -- keep doing this loop until loading complete
delay 10
if (do JavaScript "document.readyState" in document 1) is "complete" then
set pageLoaded to true
end if
end repeat
set thisURL to URL of document 1
close document 1
on error
set thisURL to "NO SITE"
try
close windows
end try
end try
end tell
-- write the result into the cell next to the key word (column K)
tell application "Microsoft Excel"
if thisURL ≠ "NO SITE" then
tell active sheet
make new hyperlink of cell ("K" & r) with properties {address:thisURL, name:thisURL}
end tell
else
tell active sheet
make new cell ("K" & r) with properties {name:"NO SITE"}
end tell
end if
end tell
set r to r + 1
end repeat
Greetings, Michael / Hamburg
Here is another solution. I tested with different delay values inside the System Events part and found a better way of getting the URL from Safari. Have a look at the two main parts of the script, everything else don't need changes:
-- type the search value into the address field and hit return (aka select and open the first proposal)
tell application "System Events"
tell process "Safari"
-- give time to prepare the window
delay 0.5
set focused of text field 1 of group 2 of toolbar 1 of window 1 to true
-- give time to prepare the address field
delay 0.5
keystroke searchTerm
-- give time to get the proposals
delay 0.5
keystroke return
end tell
end tell
-- let Safari open the suggested web page and read out the finally used URL
tell application "Safari"
-- setting the default value
set thisURL to "NO SITE"
try
-- 6 tries with 5 seconds pause (-> max. 30 sec.)
repeat 6 times
try
-- give time to load
delay 5
-- try to access the URL, if it is not available, an error occurs and the repeat loop starts again
set thisURL to URL of document 1
-- close the document
close document 1
-- no error till now, exit the repeat loop
exit repeat
end try
end repeat
on error
-- just to be sure: close all windows
try
close windows
end try
end try
end tell
Greetings, Michael / Hamburg

Resources