├── .env.example ├── .gitignore ├── .rubocop.yml ├── .vscode └── settings.json ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── install.sh ├── mac_airtag_to_mqtt.plist ├── mac_airtag_to_mqtt.rb └── restart.sh /.env.example: -------------------------------------------------------------------------------- 1 | MQTT_USERNAME=************ 2 | MQTT_PASSWORD=************ 3 | MQTT_HOST=*********** 4 | MQTT_TOPIC_NAME=YOUR_NAME 5 | MAC_USERNAME=*********** 6 | 7 | HOME_STREET_NAME="Your address street" 8 | HOME_STREET_ADDRESS="Your address number" 9 | 10 | AIRPODS_NAME="Your AirPods" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/ 43 | /vendor/bundle 44 | /lib/bundler/man/ 45 | 46 | # for a library or gem, you might want to ignore these files since the code is 47 | # intended to run in multiple environments; otherwise, check them in: 48 | # Gemfile.lock 49 | # .ruby-version 50 | # .ruby-gemset 51 | 52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 53 | .rvmrc 54 | 55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 56 | # .rubocop-https?--* 57 | 58 | .env 59 | .bundle/config 60 | python-executions 61 | tmp 62 | stdout.log 63 | stderr.log 64 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | MaxFilesInCache: 50000 3 | NewCops: enable 4 | Include: 5 | - "**/*.rb" 6 | - "Gemfile" 7 | - "Rakefile" 8 | - "lib/tasks/**/*.rake" 9 | Exclude: 10 | - "bin/**/*" 11 | - "tmp/**/*" 12 | - "db/schema.rb" 13 | - "vendor/**/*" 14 | - "ruby_encoder/**/*" 15 | - "client/node_modules/**/*" 16 | - "node_modules/**/*" 17 | - "app/views/home/code_samples/ruby.rb" 18 | - "api_clients/**/*" 19 | - "ghost_blog/**/*" 20 | - "scripts/exports/*" 21 | 22 | Style/GlobalVars: 23 | AllowedVariables: 24 | - $redis 25 | - $ci_cache_redis 26 | - $ci_cache_storage 27 | - $ci_cache_bucket 28 | 29 | # Bug in RuboCop. See: https://github.com/rubocop-hq/rubocop/issues/6588 30 | Lint/RedundantCopDisableDirective: 31 | Enabled: false 32 | 33 | Lint/RescueException: 34 | Exclude: 35 | - "lib/tasks/*.rake" 36 | 37 | Lint/SuppressedException: 38 | Enabled: false 39 | 40 | # These can be useful to prevent a case being handled by the else 41 | Lint/EmptyWhen: 42 | Enabled: false 43 | 44 | # Really annoying. I frequently return from a block 45 | # when I don't want to run any more code in the method 46 | Lint/NonLocalExitFromIterator: 47 | Enabled: false 48 | 49 | Layout/LineLength: 50 | Max: 150 51 | Exclude: 52 | - config/initializers/simple_form_bootstrap.rb 53 | - Gemfile 54 | 55 | Style/StringLiterals: 56 | EnforcedStyle: single_quotes 57 | 58 | Style/Documentation: 59 | Enabled: false 60 | 61 | Style/IfUnlessModifier: 62 | Enabled: false 63 | 64 | Metrics/AbcSize: 65 | Enabled: false 66 | 67 | Metrics/ClassLength: 68 | Enabled: false 69 | 70 | Metrics/CyclomaticComplexity: 71 | Enabled: false 72 | 73 | Metrics/PerceivedComplexity: 74 | Enabled: false 75 | 76 | Metrics/MethodLength: 77 | Enabled: false 78 | 79 | Metrics/ModuleLength: 80 | Enabled: false 81 | 82 | Metrics/ParameterLists: 83 | Enabled: false 84 | 85 | Metrics/BlockLength: 86 | Enabled: false 87 | 88 | Metrics/BlockNesting: 89 | Max: 7 90 | 91 | Style/DoubleNegation: 92 | Enabled: false 93 | 94 | Naming/FileName: 95 | Exclude: ["Guardfile", "Gemfile", "**/Gemfile"] 96 | 97 | Naming/MethodParameterName: 98 | Enabled: false 99 | 100 | Style/FormatStringToken: 101 | EnforcedStyle: annotated 102 | 103 | # Disable Style/NumericLiterals so numbers don't need underscores 104 | Style/NumericLiterals: 105 | Enabled: false 106 | 107 | Style/NumericPredicate: 108 | EnforcedStyle: comparison 109 | 110 | Style/RedundantBegin: 111 | Enabled: false 112 | 113 | # Honestly I don't care about this. 114 | Style/RegexpLiteral: 115 | Enabled: false 116 | 117 | Style/SymbolArray: 118 | Enabled: false 119 | 120 | Style/TrailingCommaInArguments: 121 | EnforcedStyleForMultiline: no_comma 122 | 123 | Style/TrailingCommaInArrayLiteral: 124 | EnforcedStyleForMultiline: consistent_comma 125 | 126 | Style/TrailingCommaInHashLiteral: 127 | EnforcedStyleForMultiline: consistent_comma 128 | 129 | # Allow UTF8 chars in comments. 130 | Style/AsciiComments: 131 | Enabled: false 132 | 133 | # Too annoying to require 'english' everywhere. 134 | Style/SpecialGlobalVars: 135 | Enabled: false 136 | 137 | Style/HashEachMethods: 138 | Enabled: false 139 | 140 | Style/HashTransformKeys: 141 | Enabled: false 142 | 143 | Style/HashTransformValues: 144 | Enabled: false 145 | 146 | Lint/RaiseException: 147 | Enabled: true 148 | 149 | Lint/StructNewOverride: 150 | Enabled: true 151 | 152 | Lint/DeprecatedOpenSSLConstant: 153 | Enabled: true 154 | Style/ExponentialNotation: 155 | Enabled: true 156 | Style/SlicingWithRange: 157 | Enabled: true 158 | 159 | # ------------------------------------------------- 160 | # Layout rules from Flexport to approximate Prettier 161 | 162 | Layout/AccessModifierIndentation: 163 | Enabled: true 164 | 165 | Layout/HashAlignment: 166 | Enabled: true 167 | EnforcedColonStyle: key 168 | EnforcedLastArgumentHashStyle: always_inspect 169 | 170 | Layout/ParameterAlignment: 171 | Enabled: true 172 | EnforcedStyle: with_first_parameter 173 | 174 | Layout/BlockAlignment: 175 | Enabled: true 176 | EnforcedStyleAlignWith: start_of_line 177 | 178 | Layout/CaseIndentation: 179 | Enabled: true 180 | EnforcedStyle: end 181 | 182 | Layout/ClosingParenthesisIndentation: 183 | Enabled: true 184 | 185 | Layout/DotPosition: 186 | EnforcedStyle: leading 187 | 188 | Layout/EmptyLineBetweenDefs: 189 | Enabled: true 190 | 191 | Layout/EmptyLines: 192 | Enabled: true 193 | 194 | Layout/EmptyLineAfterGuardClause: 195 | Enabled: false 196 | 197 | Layout/EmptyLinesAroundAccessModifier: 198 | Enabled: true 199 | 200 | Layout/EmptyLinesAroundBlockBody: 201 | Enabled: true 202 | 203 | # Note(maxh): Not sure about this one given we already have EmptyLines above. 204 | Layout/EmptyLinesAroundClassBody: 205 | Enabled: true 206 | 207 | # Why AllowForAlignment: false? 208 | # 1) Cleaner diffs. For example, when you add a longer key to a hash, 209 | # you need to update all the other rows to maintain alignment. This 210 | # means your diffs become harder to read. It looks like more is changing 211 | # than actually is. 212 | # 2) Better to have one way to do things than two. 213 | # 3) You can still use rubocop:disable comments in exceptional cases. 214 | Layout/ExtraSpacing: 215 | Enabled: true 216 | AllowForAlignment: false 217 | 218 | Layout/FirstArrayElementLineBreak: 219 | Enabled: true 220 | 221 | Layout/FirstHashElementLineBreak: 222 | Enabled: true 223 | 224 | Layout/FirstMethodArgumentLineBreak: 225 | Enabled: true 226 | 227 | Layout/HeredocArgumentClosingParenthesis: 228 | Enabled: true 229 | 230 | Layout/FirstArgumentIndentation: 231 | Enabled: true 232 | EnforcedStyle: consistent 233 | 234 | Layout/FirstArrayElementIndentation: 235 | Enabled: true 236 | EnforcedStyle: consistent 237 | 238 | Layout/FirstHashElementIndentation: 239 | Enabled: true 240 | EnforcedStyle: consistent 241 | 242 | Layout/FirstParameterIndentation: 243 | Enabled: true 244 | EnforcedStyle: consistent 245 | 246 | Layout/IndentationConsistency: 247 | Enabled: true 248 | 249 | Layout/IndentationWidth: 250 | Enabled: true 251 | 252 | Layout/LeadingCommentSpace: 253 | Enabled: true 254 | 255 | Layout/MultilineArrayLineBreaks: 256 | Enabled: true 257 | 258 | Layout/MultilineBlockLayout: 259 | Enabled: true 260 | 261 | Layout/MultilineHashBraceLayout: 262 | Enabled: true 263 | 264 | Layout/MultilineHashKeyLineBreaks: 265 | Enabled: true 266 | 267 | Layout/MultilineMethodArgumentLineBreaks: 268 | Enabled: true 269 | 270 | Layout/MultilineMethodCallBraceLayout: 271 | Enabled: true 272 | 273 | Layout/MultilineMethodCallIndentation: 274 | Enabled: true 275 | EnforcedStyle: indented 276 | 277 | Layout/MultilineOperationIndentation: 278 | Enabled: true 279 | 280 | # This doesn't play nice with private_class_method. 281 | Layout/RescueEnsureAlignment: 282 | Enabled: false 283 | 284 | Layout/SpaceAfterComma: 285 | Enabled: true 286 | 287 | Layout/SpaceAroundEqualsInParameterDefault: 288 | Enabled: true 289 | 290 | Layout/SpaceAroundOperators: 291 | Enabled: true 292 | 293 | Layout/SpaceBeforeBlockBraces: 294 | Enabled: true 295 | 296 | Layout/SpaceBeforeFirstArg: 297 | Enabled: false 298 | 299 | Layout/SpaceInsideBlockBraces: 300 | Enabled: true 301 | EnforcedStyle: space 302 | 303 | Layout/SpaceInsideHashLiteralBraces: 304 | Enabled: true 305 | EnforcedStyle: space 306 | 307 | Layout/SpaceInLambdaLiteral: 308 | Enabled: true 309 | SupportedStyles: 310 | - require_no_space 311 | 312 | # Enforce final new line. 313 | Layout/TrailingEmptyLines: 314 | Enabled: true 315 | SupportedStyles: 316 | - final_newline 317 | 318 | Layout/TrailingWhitespace: 319 | Enabled: true 320 | 321 | Layout/EndAlignment: 322 | EnforcedStyleAlignWith: variable 323 | 324 | # This seems to break some require lines somehow: 325 | Layout/EmptyLinesAroundArguments: 326 | Enabled: false 327 | 328 | Layout/EmptyLinesAroundAttributeAccessor: 329 | Enabled: true 330 | 331 | Layout/SpaceAroundMethodCallOperator: 332 | Enabled: true 333 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.trimTrailingWhitespace": true, 4 | "ruby.rubocop.executePath": "/usr/local/bin/rubocop-daemon-wrapper/", 5 | "ruby.lint": { 6 | "rubocop": false 7 | }, 8 | "[ruby]": { 9 | "editor.defaultFormatter": "misogi.ruby-rubocop" 10 | }, 11 | "ruby.rubocop.onSave": true, 12 | "ruby.rubocop.autocorrectArg": "--except Lint/UnusedMethodArgument --auto-correct-all", 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.shellcheck": true 15 | }, 16 | "shellcheck.customArgs": ["-x"], 17 | "shellcheck.useWorkspaceRootAsCwd": true 18 | } 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) do |repo_name| 5 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') 6 | "https://github.com/#{repo_name}.git" 7 | end 8 | 9 | gem 'activesupport' 10 | gem 'dotenv' 11 | gem 'mqtt' 12 | gem 'pry-byebug' 13 | gem 'rubocop' 14 | gem 'rubocop-daemon' 15 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (7.0.4) 5 | concurrent-ruby (~> 1.0, >= 1.0.2) 6 | i18n (>= 1.6, < 2) 7 | minitest (>= 5.1) 8 | tzinfo (~> 2.0) 9 | ast (2.4.2) 10 | byebug (11.1.3) 11 | coderay (1.1.3) 12 | concurrent-ruby (1.1.10) 13 | dotenv (2.8.1) 14 | i18n (1.12.0) 15 | concurrent-ruby (~> 1.0) 16 | json (2.6.2) 17 | method_source (1.0.0) 18 | minitest (5.16.3) 19 | mqtt (0.5.0) 20 | parallel (1.22.1) 21 | parser (3.1.2.1) 22 | ast (~> 2.4.1) 23 | pry (0.14.1) 24 | coderay (~> 1.1) 25 | method_source (~> 1.0) 26 | pry-byebug (3.10.1) 27 | byebug (~> 11.0) 28 | pry (>= 0.13, < 0.15) 29 | rainbow (3.1.1) 30 | regexp_parser (2.6.1) 31 | rexml (3.2.5) 32 | rubocop (1.39.0) 33 | json (~> 2.3) 34 | parallel (~> 1.10) 35 | parser (>= 3.1.2.1) 36 | rainbow (>= 2.2.2, < 4.0) 37 | regexp_parser (>= 1.8, < 3.0) 38 | rexml (>= 3.2.5, < 4.0) 39 | rubocop-ast (>= 1.23.0, < 2.0) 40 | ruby-progressbar (~> 1.7) 41 | unicode-display_width (>= 1.4.0, < 3.0) 42 | rubocop-ast (1.23.0) 43 | parser (>= 3.1.1.0) 44 | rubocop-daemon (0.3.2) 45 | rubocop 46 | ruby-progressbar (1.11.0) 47 | tzinfo (2.0.5) 48 | concurrent-ruby (~> 1.0) 49 | unicode-display_width (2.3.0) 50 | 51 | PLATFORMS 52 | x86_64-darwin-21 53 | 54 | DEPENDENCIES 55 | activesupport 56 | dotenv 57 | mqtt 58 | pry-byebug 59 | rubocop 60 | rubocop-daemon 61 | 62 | BUNDLED WITH 63 | 2.3.17 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nathan Broadbent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mac_airtag_to_mqtt 2 | 3 | Fetches AirTag data from `~/Library/Caches/com.apple.findmy.fmipcore/Items.data`, creates entities in Home Assistant with location data via [MQTT Discovery](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery). 4 | 5 | ## Running in the background 6 | 7 | You will probably need to adjust the shebang at the top of `mac_airtag_to_mqtt.rb` to point to your Ruby installation. (It was tricky to get rbenv to work with launchd.) 8 | 9 | Create launchctl plist at `/Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist`: 10 | 11 | ``` 12 | sed -e "s%/path/to/mac_airtag_to_mqtt%$PWD%g" mac_airtag_to_mqtt.plist | sudo tee /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 13 | ``` 14 | 15 | Then run: 16 | 17 | sudo launchctl load /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 18 | 19 | (To stop you need to run `launchctl unload /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist`) 20 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | bundle install 5 | 6 | sed -e "s%/path/to/mac_airtag_to_mqtt%$PWD%g" mac_airtag_to_mqtt.plist \ 7 | | sudo tee /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 8 | sudo launchctl load /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 9 | -------------------------------------------------------------------------------- /mac_airtag_to_mqtt.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.ndbroadbent.mac_airtag_to_mqtt 7 | ProgramArguments 8 | 9 | /usr/local/bin/fdautil 10 | exec 11 | /path/to/mac_airtag_to_mqtt/mac_airtag_to_mqtt.rb 12 | 13 | WorkingDirectory 14 | /path/to/mac_airtag_to_mqtt/ 15 | StandardOutPath 16 | /path/to/mac_airtag_to_mqtt/stdout.log 17 | StandardErrorPath 18 | /path/to/mac_airtag_to_mqtt/stderr.log 19 | RunAtLoad 20 | 21 | KeepAlive 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /mac_airtag_to_mqtt.rb: -------------------------------------------------------------------------------- 1 | #!/Users/ndbroadbent/.rbenv/versions/3.1.2/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | # NOTE: I couldn't figure out a nicer way to get rbenv working in the launchctl plist 5 | 6 | require 'rubygems' 7 | require 'bundler/setup' 8 | require 'json' 9 | require 'active_support' 10 | require 'active_support/core_ext' 11 | require 'active_support/time' 12 | require 'dotenv/load' 13 | require 'mqtt' 14 | require 'pry-byebug' 15 | 16 | MQTT_TOPIC_NAME = ENV.fetch('MQTT_TOPIC_NAME') 17 | MQTT_TOPIC = "mac_airtag_to_mqtt_#{MQTT_TOPIC_NAME}".freeze 18 | AIRTAGS_DATA_FILE = "/Users/#{ENV.fetch('MAC_USERNAME')}/Library/Caches/com.apple.findmy.fmipcore/Items.data".freeze 19 | 20 | # DEBUG = true 21 | DEBUG = false 22 | 23 | loop do 24 | begin 25 | port = ENV.fetch('MQTT_PORT', 1883) 26 | puts "Connecting to MQTT broker at #{ENV.fetch('MQTT_HOST')}:#{port}..." 27 | client = MQTT::Client.connect( 28 | host: ENV.fetch('MQTT_HOST'), 29 | port:, 30 | username: ENV.fetch('MQTT_USERNAME'), 31 | password: ENV.fetch('MQTT_PASSWORD'), 32 | will_topic: "#{MQTT_TOPIC}/status", 33 | will_payload: 'offline', 34 | will_qos: 1, 35 | will_retain: true 36 | ) 37 | puts 'Connected!' 38 | 39 | client.publish( 40 | "homeassistant/binary_sensor/#{MQTT_TOPIC}/connectivity/config", 41 | { 42 | name: 'Mac Airtag To MQTT', 43 | uniq_id: "#{MQTT_TOPIC}_connectivity", 44 | stat_t: "#{MQTT_TOPIC}/status", 45 | dev_cla: 'connectivity', 46 | pl_on: 'online', 47 | pl_off: 'offline', 48 | }.to_json 49 | ) 50 | client.publish( 51 | "#{MQTT_TOPIC}/status", 52 | 'online' 53 | ) 54 | 55 | loop do 56 | puts "Reading airtags data from #{AIRTAGS_DATA_FILE}..." if DEBUG 57 | airtags = JSON.parse(File.read(AIRTAGS_DATA_FILE)) 58 | puts "Publishing MQTT messages for #{airtags.count} airtags..." if DEBUG 59 | airtags.each do |airtag| 60 | state_topic = "#{MQTT_TOPIC}/#{airtag['identifier']}/state" 61 | json_attributes_topic = "#{MQTT_TOPIC}/#{airtag['identifier']}/attributes" 62 | ha_config_topic = "homeassistant/device_tracker/#{MQTT_TOPIC}_#{airtag['identifier']}/config" 63 | 64 | name = airtag['name'] 65 | location = airtag['location'] || {} 66 | address = airtag['address'] || {} 67 | 68 | name = if name.end_with?('Bud') 69 | "#{ENV.fetch('AIRPODS_NAME')} - #{name}" 70 | else 71 | "AirTag - #{name}" 72 | end 73 | 74 | is_home = address['streetName'] == ENV.fetch('HOME_STREET_NAME') && 75 | address['streetAddress']&.start_with?(ENV.fetch('HOME_STREET_ADDRESS')) 76 | 77 | puts "=> #{ha_config_topic}: #{name}" if DEBUG 78 | client.publish( 79 | ha_config_topic, 80 | { 81 | state_topic:, 82 | name:, 83 | unique_id: "#{MQTT_TOPIC}_#{airtag['identifier']}", 84 | payload_home: 'home', 85 | payload_not_home: 'not_home', 86 | json_attributes_topic:, 87 | }.to_json 88 | ) 89 | 90 | client.publish( 91 | state_topic, 92 | is_home ? 'home' : 'not_home' 93 | ) 94 | 95 | client.publish( 96 | json_attributes_topic, 97 | { 98 | latitude: location['latitude'], 99 | longitude: location['longitude'], 100 | gps_accuracy: location['horizontalAccuracy'], 101 | address: address['mapItemFullAddress'], 102 | device_type: 'Apple AirTag', 103 | }.to_json 104 | ) 105 | end 106 | 107 | sleep 30 108 | end 109 | rescue StandardError => e 110 | puts "Error: #{e.message}" 111 | puts 'Waiting 15 seconds before retrying...' 112 | sleep 15 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo launchctl unload /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 3 | sudo launchctl load /Library/LaunchDaemons/com.ndbroadbent.mac_airtag_to_mqtt.plist 4 | --------------------------------------------------------------------------------