2016-12-27 10:32:49 -07:00
|
|
|
# Farmbot Device models all data related to an actual FarmBot in the real world.
|
2017-07-28 08:47:15 -06:00
|
|
|
class Device < ApplicationRecord
|
2019-05-22 10:21:09 -06:00
|
|
|
DEFAULT_MAX_CONFIGS = 300
|
2019-03-11 16:54:39 -06:00
|
|
|
DEFAULT_MAX_IMAGES = 100
|
|
|
|
DEFAULT_MAX_LOGS = 1000
|
|
|
|
|
|
|
|
TIMEZONES = TZInfo::Timezone.all_identifiers
|
|
|
|
BAD_TZ = "%{value} is not a valid timezone"
|
2019-11-08 06:45:00 -07:00
|
|
|
BAD_OTA_HOUR = "must be a value from 0 to 23."
|
2019-03-11 16:54:39 -06:00
|
|
|
THROTTLE_ON = "Device is sending too many logs (%s). " \
|
|
|
|
"Suspending log storage and display until %s."
|
|
|
|
THROTTLE_OFF = "Cooldown period has ended. " \
|
|
|
|
"Resuming log storage."
|
|
|
|
|
2019-05-02 18:20:22 -06:00
|
|
|
PLURAL_RESOURCES = %i(alerts farmware_envs farm_events farmware_installations
|
|
|
|
images logs peripherals pin_bindings plant_templates
|
2019-08-05 09:02:32 -06:00
|
|
|
points point_groups regimens saved_gardens
|
|
|
|
sensor_readings sensors sequences token_issuances tools
|
2020-02-04 12:42:40 -07:00
|
|
|
webcam_feeds fragments)
|
2019-05-02 18:20:22 -06:00
|
|
|
|
|
|
|
PLURAL_RESOURCES.map { |resources| has_many resources, dependent: :destroy }
|
|
|
|
|
|
|
|
SINGULAR_RESOURCES = {
|
|
|
|
fbos_config: FbosConfig,
|
|
|
|
firmware_config: FirmwareConfig,
|
|
|
|
web_app_config: WebAppConfig,
|
|
|
|
}
|
|
|
|
|
|
|
|
SINGULAR_RESOURCES.map do |(name, klass)|
|
|
|
|
has_one name, dependent: :destroy
|
|
|
|
define_method(name) { super() || klass.create!(device: self) }
|
|
|
|
end
|
|
|
|
|
2019-03-11 16:54:39 -06:00
|
|
|
has_many :in_use_tools
|
|
|
|
has_many :in_use_points
|
|
|
|
has_many :users
|
2019-12-03 12:36:41 -07:00
|
|
|
has_many :folders
|
2018-07-11 09:35:17 -06:00
|
|
|
|
2017-08-18 09:04:37 -06:00
|
|
|
validates_presence_of :name
|
2019-05-02 18:20:22 -06:00
|
|
|
validates :timezone, inclusion: {
|
|
|
|
in: TIMEZONES,
|
|
|
|
message: BAD_TZ,
|
|
|
|
allow_nil: true,
|
|
|
|
}
|
2019-11-07 12:35:10 -07:00
|
|
|
validates :ota_hour,
|
2019-11-08 06:45:00 -07:00
|
|
|
inclusion: { in: [*0..23], message: BAD_OTA_HOUR, allow_nil: true }
|
2018-05-21 11:48:24 -06:00
|
|
|
|
2017-02-07 12:00:39 -07:00
|
|
|
# Give the user back the amount of logs they are allowed to view.
|
|
|
|
def limited_log_list
|
2018-04-04 12:55:12 -06:00
|
|
|
Log
|
|
|
|
.order(created_at: :desc)
|
|
|
|
.where(device_id: self.id)
|
|
|
|
.limit(max_log_count || DEFAULT_MAX_LOGS)
|
2016-12-06 15:22:18 -07:00
|
|
|
end
|
2017-05-08 15:34:03 -06:00
|
|
|
|
2019-02-08 16:56:08 -07:00
|
|
|
def excess_logs
|
|
|
|
Log
|
|
|
|
.where
|
|
|
|
.not(id: limited_log_list.pluck(:id))
|
|
|
|
.where(device_id: self.id)
|
|
|
|
end
|
|
|
|
|
2017-10-31 09:28:52 -06:00
|
|
|
def self.current
|
2017-11-06 14:55:08 -07:00
|
|
|
RequestStore.store[:device]
|
2017-10-31 09:28:52 -06:00
|
|
|
end
|
|
|
|
|
2017-11-05 10:36:24 -07:00
|
|
|
def self.current=(dev)
|
2017-11-06 14:55:08 -07:00
|
|
|
RequestStore.store[:device] = dev
|
2017-11-05 10:36:24 -07:00
|
|
|
end
|
2017-11-08 07:31:43 -07:00
|
|
|
# Sets Device.current to `self` and returns it to the previous value when
|
2019-02-19 19:10:08 -07:00
|
|
|
# finished running block. Usually this is unnecessary, but may be required in
|
2017-11-08 07:31:43 -07:00
|
|
|
# background jobs. If you are not receiving auto_sync data on your client,
|
|
|
|
# you probably need to use this method.
|
|
|
|
def auto_sync_transaction
|
2019-03-11 16:54:39 -06:00
|
|
|
prev = Device.current
|
2017-11-08 07:31:43 -07:00
|
|
|
Device.current = self
|
|
|
|
yield
|
|
|
|
Device.current = prev
|
2017-10-31 09:28:52 -06:00
|
|
|
end
|
2017-12-28 08:36:48 -07:00
|
|
|
|
|
|
|
def tz_offset_hrs
|
|
|
|
Time.now.in_time_zone(self.timezone || "UTC").utc_offset / 1.hour
|
|
|
|
end
|
2018-03-31 10:30:46 -06:00
|
|
|
|
2018-04-20 07:49:21 -06:00
|
|
|
def plants
|
|
|
|
points.where(pointer_type: "Plant")
|
|
|
|
end
|
2018-05-20 14:04:38 -06:00
|
|
|
|
2019-04-30 12:11:39 -06:00
|
|
|
def tool_slots
|
|
|
|
points.where(pointer_type: "ToolSlot")
|
|
|
|
end
|
|
|
|
|
2019-05-02 18:20:22 -06:00
|
|
|
def generic_pointers
|
|
|
|
points.where(pointer_type: "GenericPointer")
|
|
|
|
end
|
|
|
|
|
2019-09-19 13:15:09 -06:00
|
|
|
# Sets the `throttled_until` and `throttled_at` fields if unpopulated or
|
|
|
|
# the throttle time period increases. Notifies user of cooldown period.
|
2018-05-22 14:36:05 -06:00
|
|
|
def maybe_throttle(violation)
|
2019-09-19 13:15:09 -06:00
|
|
|
end_t = violation.ends_at
|
2018-05-22 12:41:28 -06:00
|
|
|
# Some log validation errors will result in until_time being `nil`.
|
2019-10-31 14:42:45 -06:00
|
|
|
if (throttled_until.nil? || (end_t > throttled_until))
|
2019-10-30 15:28:59 -06:00
|
|
|
reload.update!(throttled_until: end_t, throttled_at: Time.now)
|
2019-09-19 13:15:09 -06:00
|
|
|
cooldown = end_t.in_time_zone(self.timezone || "UTC").strftime("%I:%M%p")
|
2018-05-22 14:36:05 -06:00
|
|
|
info = [violation.explanation, cooldown]
|
2019-09-19 13:15:09 -06:00
|
|
|
cooldown_notice(THROTTLE_ON % info, end_t, "warn")
|
2018-05-20 17:50:54 -06:00
|
|
|
end
|
2018-05-20 14:04:38 -06:00
|
|
|
end
|
|
|
|
|
2018-05-20 16:17:59 -06:00
|
|
|
def maybe_unthrottle
|
2018-05-21 10:51:39 -06:00
|
|
|
if throttled_until.present?
|
2018-05-21 13:32:02 -06:00
|
|
|
old_time = throttled_until
|
2019-10-30 14:07:52 -06:00
|
|
|
update!(throttled_until: nil, throttled_at: nil)
|
2018-05-21 14:40:15 -06:00
|
|
|
cooldown_notice(THROTTLE_OFF, old_time, "info")
|
2018-05-20 17:50:54 -06:00
|
|
|
end
|
2018-05-20 14:04:38 -06:00
|
|
|
end
|
2019-03-11 16:54:39 -06:00
|
|
|
|
2018-05-22 12:49:33 -06:00
|
|
|
# Send a realtime message to a logged in user.
|
|
|
|
def tell(message, channels = [], type = "info")
|
2019-03-11 16:54:39 -06:00
|
|
|
log = Log.new({ device: self,
|
|
|
|
message: message,
|
|
|
|
created_at: Time.now,
|
|
|
|
channels: channels,
|
|
|
|
major_version: 99,
|
|
|
|
minor_version: 99,
|
|
|
|
meta: {},
|
|
|
|
type: type })
|
2018-05-22 12:49:33 -06:00
|
|
|
json = LogSerializer.new(log).as_json.to_json
|
|
|
|
|
|
|
|
Transport.current.amqp_send(json, self.id, "logs")
|
2018-05-22 15:02:22 -06:00
|
|
|
return log
|
2018-05-22 12:49:33 -06:00
|
|
|
end
|
2018-05-21 13:32:02 -06:00
|
|
|
|
2018-05-21 14:40:15 -06:00
|
|
|
def cooldown_notice(message, throttle_time, type, now = Time.current)
|
2019-03-11 16:54:39 -06:00
|
|
|
hours = ((throttle_time - now) / 1.hour).round
|
2018-05-22 07:05:39 -06:00
|
|
|
channels = [(hours > 2) ? "email" : "toast"]
|
2019-03-11 16:54:39 -06:00
|
|
|
tell(message, channels, type).save
|
2018-05-21 13:32:02 -06:00
|
|
|
end
|
2018-05-22 07:42:01 -06:00
|
|
|
|
2018-07-17 14:50:09 -06:00
|
|
|
def regimina
|
|
|
|
regimens # :(
|
|
|
|
end
|
|
|
|
|
2018-05-22 10:30:29 -06:00
|
|
|
# CONTEXT:
|
|
|
|
# * We tried to use Rails low level caching, but it hit marshalling issues.
|
|
|
|
# * We did a hack with Device.new(self.as_json) to get around it.
|
|
|
|
# * Mutations does not allow unsaved models
|
|
|
|
# * We converted the `model :device, class: Device` to:
|
|
|
|
# `duck :device, methods [:id, :is_device]`
|
|
|
|
#
|
2019-02-19 19:10:08 -07:00
|
|
|
# This method is not required, but adds a layer of safety.
|
2018-05-22 07:42:01 -06:00
|
|
|
def is_device # SEE: Hack in Log::Create. TODO: Fix low level caching bug.
|
|
|
|
true
|
|
|
|
end
|
2018-08-16 08:59:38 -06:00
|
|
|
|
|
|
|
def unsent_routine_emails
|
|
|
|
logs
|
|
|
|
.where(sent_at: nil)
|
|
|
|
.where(Log::IS_EMAIL_ISH) # `email` and `fatal_email`
|
|
|
|
.where
|
|
|
|
.not(Log::IS_FATAL_EMAIL) # Filter out `fatal_email`s
|
|
|
|
.order(created_at: :desc)
|
|
|
|
end
|
2019-03-07 13:21:54 -07:00
|
|
|
|
|
|
|
# Helper method to create an auth token.
|
|
|
|
# Used by sys admins to debug problems without performing a password reset.
|
2019-10-02 11:14:13 -06:00
|
|
|
def help_customer
|
2019-03-11 16:54:39 -06:00
|
|
|
Rollbar.error("Someone is creating a debug user token", { device: self.id })
|
2019-10-02 11:14:13 -06:00
|
|
|
token = SessionToken.as_json(users.first, "staff", fbos_version).to_json
|
|
|
|
return "localStorage['session'] = JSON.stringify(#{token});"
|
2019-03-07 13:21:54 -07:00
|
|
|
end
|
2019-07-10 11:57:31 -06:00
|
|
|
|
|
|
|
TOO_MANY_CONNECTIONS =
|
2020-02-12 17:35:02 -07:00
|
|
|
"Your device is reconnecting to the server too often. " +
|
|
|
|
"This may be a sign of local network issues. " +
|
|
|
|
"Please review the documentation provided at " +
|
|
|
|
"https://software.farm.bot/docs/connecting-farmbot-to-the-internet"
|
2019-07-10 11:57:31 -06:00
|
|
|
def self.connection_warning(username)
|
|
|
|
device_id = username.split("_").last.to_i || 0
|
2019-07-15 16:08:51 -06:00
|
|
|
device = self.find_by(id: device_id)
|
|
|
|
return unless device
|
|
|
|
|
|
|
|
last_sent_at = device.mqtt_rate_limit_email_sent_at || 4.years.ago
|
|
|
|
if last_sent_at < 1.day.ago
|
2019-10-23 10:33:30 -06:00
|
|
|
device.update!(mqtt_rate_limit_email_sent_at: Time.now)
|
2019-07-15 16:08:51 -06:00
|
|
|
device.tell(TOO_MANY_CONNECTIONS, ["fatal_email"])
|
|
|
|
end
|
2019-07-10 11:57:31 -06:00
|
|
|
end
|
2014-05-09 07:48:18 -06:00
|
|
|
end
|