Default behavior for reference registry changed to strong references. PKT8 test for numass is working.
This commit is contained in:
parent
d77051b54f
commit
f0fc22c9ec
@ -7,6 +7,7 @@ syntax: glob
|
||||
build/*
|
||||
target/*
|
||||
private/*
|
||||
out/*
|
||||
.gradle/*
|
||||
.nb-gradle/*
|
||||
.idea/
|
||||
|
@ -3,16 +3,17 @@ package inr.numass.control
|
||||
import hep.dataforge.context.Context
|
||||
import hep.dataforge.context.Global
|
||||
import hep.dataforge.control.DeviceManager
|
||||
import hep.dataforge.control.connections.Roles
|
||||
import hep.dataforge.control.devices.Device
|
||||
import hep.dataforge.kodex.useMeta
|
||||
import hep.dataforge.kodex.useMetaList
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.server.ServerManager
|
||||
import hep.dataforge.storage.api.Storage
|
||||
import hep.dataforge.storage.commons.StorageConnection
|
||||
import hep.dataforge.storage.commons.StorageManager
|
||||
import inr.numass.client.ClientUtils
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import tornadofx.*
|
||||
@ -25,32 +26,9 @@ class BoardController() : Controller(), AutoCloseable {
|
||||
|
||||
val contextProperty = SimpleObjectProperty<Context>(Global.instance())
|
||||
var context: Context by contextProperty
|
||||
|
||||
// val metaProperty = SimpleObjectProperty<Meta>(Meta.empty())
|
||||
// var meta: Meta by metaProperty
|
||||
|
||||
val numassRunProperty = SimpleStringProperty("")
|
||||
var numassRun: String by numassRunProperty
|
||||
private set
|
||||
|
||||
val storageProperty = nonNullObjectBinding(contextProperty, numassRunProperty) {
|
||||
val rootStorage = context.pluginManager.getOrLoad(StorageManager::class.java).defaultStorage
|
||||
|
||||
if (!numassRun.isEmpty()) {
|
||||
context.logger.info("Run information found. Selecting run {}", numassRun)
|
||||
rootStorage.buildShelf(numassRun, Meta.empty());
|
||||
} else {
|
||||
rootStorage
|
||||
}
|
||||
}.apply {
|
||||
onChange {
|
||||
val connection = StorageConnection(value)
|
||||
devices.forEach { device ->
|
||||
device.forEachConnection(StorageConnection::class.java) { device.disconnect(it) }//removing all ald storage connections
|
||||
device.connect(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
val storageProperty = SimpleObjectProperty<Storage>(null)
|
||||
|
||||
val serverManagerProperty = objectBinding(contextProperty) {
|
||||
context.optFeature(ServerManager::class.java).orElse(null)
|
||||
@ -58,52 +36,37 @@ class BoardController() : Controller(), AutoCloseable {
|
||||
|
||||
val devices: ObservableList<Device> = FXCollections.observableArrayList();
|
||||
|
||||
val deviceManagerProperty = objectBinding(contextProperty) {
|
||||
context.optFeature(DeviceManager::class.java).orElse(null)
|
||||
}.apply {
|
||||
onChange {
|
||||
value?.let {
|
||||
devices.setAll(it.devices.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// val deviceViews: ObservableList<DeviceDisplay<*>> = object : ListBinding<DeviceDisplay<*>>() {
|
||||
// init {
|
||||
// bind(devices)
|
||||
// }
|
||||
//
|
||||
// override fun computeValue(): ObservableList<DeviceDisplay<*>> {
|
||||
// val manager = deviceManagerProperty.value
|
||||
// return if (manager == null) {
|
||||
// FXCollections.emptyObservableList();
|
||||
// } else {
|
||||
// manager.deviceNames()
|
||||
// .filter { it.length == 1 } // select top level devices
|
||||
// .map { manager.optDevice(it) }
|
||||
// .filter { it.isPresent }
|
||||
// .map { it.get().getDisplay() }
|
||||
// .toList().observable()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun configure(meta: Meta) {
|
||||
Context.build("NUMASS", Global.instance(), meta.getMeta("context", meta)).apply {
|
||||
val numassRun = meta.optMeta("numass").map { ClientUtils.getRunName(it) }.orElse("")
|
||||
|
||||
meta.useMeta("storage") {
|
||||
pluginManager.getOrLoad(StorageManager::class.java).configure(it);
|
||||
}
|
||||
|
||||
val rootStorage = pluginManager.getOrLoad(StorageManager::class.java).defaultStorage
|
||||
|
||||
val storage = if (!numassRun.isEmpty()) {
|
||||
logger.info("Run information found. Selecting run {}", numassRun)
|
||||
rootStorage.buildShelf(numassRun, Meta.empty());
|
||||
} else {
|
||||
rootStorage
|
||||
}
|
||||
val connection = StorageConnection(storage)
|
||||
|
||||
val deviceManager = pluginManager.getOrLoad(DeviceManager::class.java)
|
||||
|
||||
meta.useMetaList("device") {
|
||||
it.forEach {
|
||||
pluginManager.getOrLoad(DeviceManager::class.java).buildDevice(it)
|
||||
deviceManager.buildDevice(it)
|
||||
}
|
||||
}
|
||||
meta.useMeta("numass") {
|
||||
numassRun = ClientUtils.getRunName(it)
|
||||
}
|
||||
deviceManager.devices.forEach { it.connect(connection, Roles.STORAGE_ROLE, Roles.MEASUREMENT_LISTENER_ROLE) }
|
||||
}.also {
|
||||
runLater {
|
||||
context = it
|
||||
devices.setAll(context.getFeature(DeviceManager::class.java).devices.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,89 +75,4 @@ class BoardController() : Controller(), AutoCloseable {
|
||||
context.close()
|
||||
//Global.terminate()
|
||||
}
|
||||
// val devices: ObservableList<DeviceDisplay<*>> = FXCollections.observableArrayList<DeviceDisplay<*>>();
|
||||
//
|
||||
// val contextProperty = SimpleObjectProperty<Context>(Global.instance())
|
||||
// var context: Context by contextProperty
|
||||
// private set
|
||||
//
|
||||
// val storageProperty = SimpleObjectProperty<Storage>()
|
||||
// var storage: Storage? by storageProperty
|
||||
// private set
|
||||
//
|
||||
// val serverManagerProperty = SimpleObjectProperty<ServerManager>()
|
||||
// var serverManager: ServerManager? by serverManagerProperty
|
||||
// private set
|
||||
//
|
||||
// fun load(app: Application) {
|
||||
// runAsync {
|
||||
// getConfig(app).ifPresent {
|
||||
// val context = Context.build("NUMASS", Global.instance(), it)
|
||||
// load(context, it)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// private fun load(context: Context, meta: Meta) {
|
||||
// this.context = context;
|
||||
// devices.clear();
|
||||
// meta.getMetaList("device").forEach {
|
||||
// try {
|
||||
// Platform.runLater { devices.add(buildDeviceView(context, it)) };
|
||||
// } catch (ex: Exception) {
|
||||
// context.logger.error("Can't build device view", ex);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (meta.hasMeta("storage")) {
|
||||
// val st = buildStorage(context, meta);
|
||||
// val storageConnection = StorageConnection(storage);
|
||||
// devices.forEach {
|
||||
// if (it.device.acceptsRole(Roles.STORAGE_ROLE)) {
|
||||
// it.device.connect(storageConnection, Roles.STORAGE_ROLE);
|
||||
// }
|
||||
// }
|
||||
// Platform.runLater {
|
||||
// storage = st
|
||||
// meta.optMeta("server").ifPresent { serverMeta ->
|
||||
// val sm = context.getPluginManager().getOrLoad(ServerManager::class.java);
|
||||
// sm.configure(serverMeta)
|
||||
//
|
||||
// sm.bind(NumassStorageServerObject(serverManager, storage, "numass-storage"));
|
||||
// serverManager = sm
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun buildDeviceView(context: Context, deviceMeta: Meta): DeviceDisplay<*> {
|
||||
// context.logger.info("Building device with meta: {}", deviceMeta)
|
||||
// val device = context.loadFeature("devices", DeviceManager::class.java).buildDevice(deviceMeta)
|
||||
// device.init();
|
||||
// return device.getDisplay();
|
||||
// }
|
||||
//
|
||||
// private fun buildStorage(context: Context, meta: Meta): Storage {
|
||||
// val storageMeta = meta.getMeta("storage").builder
|
||||
// .putValue("readOnly", false)
|
||||
// .putValue("monitor", true)
|
||||
//
|
||||
// context.logger.info("Creating storage for server with meta {}", storageMeta)
|
||||
// var storage = StorageFactory.buildStorage(context, storageMeta);
|
||||
//
|
||||
// val numassRun = ClientUtils.getRunName(meta)
|
||||
// if (!numassRun.isEmpty()) {
|
||||
// context.logger.info("Run information found. Selecting run {}", numassRun)
|
||||
// storage = storage.buildShelf(numassRun, Meta.empty());
|
||||
// }
|
||||
// return storage;
|
||||
// }
|
||||
//
|
||||
// override fun close() {
|
||||
// devices.forEach {
|
||||
// it.close()
|
||||
// }
|
||||
// context.close();
|
||||
// }
|
||||
}
|
@ -22,9 +22,8 @@ import hep.dataforge.meta.Metoid
|
||||
import hep.dataforge.names.Named
|
||||
|
||||
|
||||
internal fun createChannel(name: String): PKT8Channel {
|
||||
return PKT8Channel(MetaBuilder("channel").putValue("name", name)) { d -> d }
|
||||
}
|
||||
internal fun createChannel(name: String): PKT8Channel =
|
||||
PKT8Channel(MetaBuilder("channel").putValue("name", name)) { d -> d }
|
||||
|
||||
internal fun createChannel(meta: Meta): PKT8Channel {
|
||||
val transformationType = meta.getString("transformationType", "default")
|
||||
|
@ -42,7 +42,6 @@ import inr.numass.control.DeviceView
|
||||
import inr.numass.control.StorageHelper
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import kotlin.streams.toList
|
||||
|
||||
|
||||
/**
|
||||
@ -61,14 +60,14 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
/**
|
||||
* The key is the letter (a,b,c,d...) as in measurements
|
||||
*/
|
||||
private val channels = LinkedHashMap<String, PKT8Channel>()
|
||||
val channels = LinkedHashMap<String, PKT8Channel>()
|
||||
private var collector: RegularPointCollector? = null
|
||||
private var storageHelper: StorageHelper? = null
|
||||
|
||||
/**
|
||||
* Cached values
|
||||
*/
|
||||
private var format: TableFormat? = null
|
||||
//private var format: TableFormat? = null
|
||||
|
||||
|
||||
private// Building data format
|
||||
@ -76,15 +75,12 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
val tableFormatBuilder = TableFormatBuilder()
|
||||
.addTime("timestamp")
|
||||
|
||||
for (channel in channels.values) {
|
||||
for (channel in this.channels.values) {
|
||||
tableFormatBuilder.addNumber(channel.name)
|
||||
}
|
||||
tableFormatBuilder.build()
|
||||
}
|
||||
|
||||
val chanels: Collection<PKT8Channel>
|
||||
get() = this.channels.values
|
||||
|
||||
val sps: String
|
||||
get() = getState(SPS).stringValue()
|
||||
|
||||
@ -119,7 +115,7 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
} else {
|
||||
//set default channel configuration
|
||||
for (designation in CHANNEL_DESIGNATIONS) {
|
||||
channels.put(designation, createChannel(designation))
|
||||
this.channels.put(designation, createChannel(designation))
|
||||
}
|
||||
logger.warn("No channels defined in configuration")
|
||||
}
|
||||
@ -143,12 +139,9 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
// setting up the collector
|
||||
storageHelper = StorageHelper(this) { connection: StorageConnection -> this.buildLoader(connection) }
|
||||
val duration = Duration.parse(meta().getString("averagingDuration", "PT30S"))
|
||||
collector = RegularPointCollector(
|
||||
duration,
|
||||
channels.values.stream().map { it.name }.toList()
|
||||
) { dp: Values ->
|
||||
collector = RegularPointCollector(duration, this.channels.values.map { it.name }) { dp: Values ->
|
||||
logger.debug("Point measurement complete. Pushing...")
|
||||
storageHelper!!.push(dp)
|
||||
storageHelper?.push(dp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,13 +155,12 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
|
||||
@Throws(ControlException::class)
|
||||
override fun buildHandler(portName: String): PortHandler {
|
||||
val handler: PortHandler
|
||||
//setup connection
|
||||
if ("virtual" == portName) {
|
||||
val handler: PortHandler = if ("virtual" == portName) {
|
||||
logger.info("Starting {} using virtual debug port", name)
|
||||
handler = PKT8VirtualPort("PKT8", meta().getMetaOrEmpty("debug"))
|
||||
PKT8VirtualPort("PKT8", meta().getMetaOrEmpty("debug"))
|
||||
} else {
|
||||
handler = super.buildHandler(portName)
|
||||
super.buildHandler(portName)
|
||||
}
|
||||
handler.setDelimiter("\n")
|
||||
|
||||
@ -209,17 +201,17 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
* @return
|
||||
*/
|
||||
private fun spsToStr(sps: Int): String {
|
||||
when (sps) {
|
||||
0 -> return "2.5 SPS"
|
||||
1 -> return "5 SPS"
|
||||
2 -> return "10 SPS"
|
||||
3 -> return "25 SPS"
|
||||
4 -> return "50 SPS"
|
||||
5 -> return "100 SPS"
|
||||
6 -> return "500 SPS"
|
||||
7 -> return "1 kSPS"
|
||||
8 -> return "3.75 kSPS"
|
||||
else -> return "unknown value"
|
||||
return when (sps) {
|
||||
0 -> "2.5 SPS"
|
||||
1 -> "5 SPS"
|
||||
2 -> "10 SPS"
|
||||
3 -> "25 SPS"
|
||||
4 -> "50 SPS"
|
||||
5 -> "100 SPS"
|
||||
6 -> "500 SPS"
|
||||
7 -> "1 kSPS"
|
||||
8 -> "3.75 kSPS"
|
||||
else -> "unknown value"
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,15 +223,15 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
* @return
|
||||
*/
|
||||
private fun pgaToStr(pga: Int): String {
|
||||
when (pga) {
|
||||
0 -> return "± 5 V"
|
||||
1 -> return "± 2,5 V"
|
||||
2 -> return "± 1,25 V"
|
||||
3 -> return "± 0,625 V"
|
||||
4 -> return "± 312.5 mV"
|
||||
5 -> return "± 156.25 mV"
|
||||
6 -> return "± 78.125 mV"
|
||||
else -> return "unknown value"
|
||||
return when (pga) {
|
||||
0 -> "± 5 V"
|
||||
1 -> "± 2,5 V"
|
||||
2 -> "± 1,25 V"
|
||||
3 -> "± 0,625 V"
|
||||
4 -> "± 312.5 mV"
|
||||
5 -> "± 156.25 mV"
|
||||
6 -> "± 78.125 mV"
|
||||
else -> "unknown value"
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,7 +248,7 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
updateState(SPS, Integer.parseInt(response.substring(4)))
|
||||
// getLogger().info("successfully sampling rate to {}", spsToStr(this.sps));
|
||||
} else {
|
||||
logger.error("Setting sps failsed with message: " + response)
|
||||
logger.error("Setting sps failed with message: " + response)
|
||||
}
|
||||
}
|
||||
|
||||
@ -309,7 +301,7 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
try {
|
||||
logger.info("Starting measurement")
|
||||
handler.holdBy(this)
|
||||
handler.send(this,"s")
|
||||
handler.send(this, "s")
|
||||
afterStart()
|
||||
} catch (ex: ControlException) {
|
||||
portError("Failed to start measurement", ex)
|
||||
@ -332,9 +324,8 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
error(ex)
|
||||
false
|
||||
} finally {
|
||||
if (collector != null) {
|
||||
collector!!.clear()
|
||||
}
|
||||
collector?.clear()
|
||||
logger.debug("Removing port lock")
|
||||
handler.unholdBy(this)
|
||||
}
|
||||
}
|
||||
@ -351,13 +342,11 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
val designation = trimmed.substring(0, 1)
|
||||
val rawValue = java.lang.Double.parseDouble(trimmed.substring(1)) / 100
|
||||
|
||||
val channel = channels[designation]
|
||||
val channel = this@PKT8Device.channels[designation]
|
||||
|
||||
if (channel != null) {
|
||||
result(channel.evaluate(rawValue))
|
||||
if (collector != null) {
|
||||
collector!!.put(channel.getName(), channel.getTemperature(rawValue))
|
||||
}
|
||||
collector?.put(channel.name, channel.getTemperature(rawValue))
|
||||
} else {
|
||||
result(PKT8Result(designation, rawValue, -1.0))
|
||||
}
|
||||
@ -380,4 +369,12 @@ class PKT8Device(context: Context, meta: Meta) : PortSensor<PKT8Result>(context,
|
||||
private val CHANNEL_DESIGNATIONS = arrayOf("a", "b", "c", "d", "e", "f", "g", "h")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
data class PKT8Result(val channel: String, val rawValue: Double, val temperature: Double) {
|
||||
|
||||
val rawString: String = String.format("%.2f", rawValue)
|
||||
|
||||
val temperatureString: String = String.format("%.2f", temperature)
|
||||
}
|
@ -7,6 +7,7 @@ import hep.dataforge.fx.bindWindow
|
||||
import hep.dataforge.fx.fragments.LogFragment
|
||||
import hep.dataforge.fx.plots.PlotContainer
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.plots.Plot
|
||||
import hep.dataforge.plots.PlotFrame
|
||||
import hep.dataforge.plots.PlotUtils
|
||||
import hep.dataforge.plots.data.TimePlot
|
||||
@ -135,16 +136,14 @@ class PKT8Display : DeviceDisplay<PKT8Device>(), MeasurementListener {
|
||||
}
|
||||
|
||||
inner class CryoPlotView : View("PKT8 temperature plot") {
|
||||
val plotFrameMeta: Meta = device.meta.getMetaOrEmpty("plotConfig")
|
||||
private val plotFrameMeta: Meta = device.meta.getMetaOrEmpty("plotConfig")
|
||||
|
||||
val plotFrame: PlotFrame by lazy {
|
||||
private val plotFrame: PlotFrame by lazy {
|
||||
JFreeChartFrame(plotFrameMeta).apply {
|
||||
PlotUtils.setXAxis(this, "timestamp", null, "time")
|
||||
}
|
||||
}
|
||||
|
||||
private val plottables = plotFrame.plots
|
||||
|
||||
var rawDataButton: ToggleButton by singleAssign()
|
||||
|
||||
override val root: Parent = borderpane {
|
||||
@ -169,39 +168,43 @@ class PKT8Display : DeviceDisplay<PKT8Device>(), MeasurementListener {
|
||||
}
|
||||
|
||||
init {
|
||||
val channels = device.chanels
|
||||
|
||||
//frame config from device configuration
|
||||
//Do not use view config here, it is applyed separately
|
||||
channels.stream()
|
||||
.filter { channel -> !plottables.has(channel.name) }
|
||||
.forEachOrdered { channel ->
|
||||
//frame config from device configuration
|
||||
val plot = TimePlot(channel.name)
|
||||
plot.configure(channel.meta())
|
||||
plottables.add(plot)
|
||||
plotFrame.add(plot)
|
||||
}
|
||||
if (device.meta().hasMeta("plotConfig")) {
|
||||
plottables.configure(device.meta().getMeta("plotConfig"))
|
||||
TimePlot.setMaxItems(plottables, 1000)
|
||||
TimePlot.setPrefItems(plottables, 400)
|
||||
with(plotFrame.plots) {
|
||||
configure(device.meta().getMeta("plotConfig"))
|
||||
TimePlot.setMaxItems(this, 1000)
|
||||
TimePlot.setPrefItems(this, 400)
|
||||
}
|
||||
}
|
||||
table.addListener(MapChangeListener { change ->
|
||||
if (change.wasAdded()) {
|
||||
change.valueAdded.apply {
|
||||
if (rawDataButton.isSelected) {
|
||||
plottables.opt(channel).ifPresent { TimePlot.put(it, rawValue) }
|
||||
} else {
|
||||
plottables.opt(channel).ifPresent { TimePlot.put(it, temperature) }
|
||||
getPlot(channel)?.apply {
|
||||
if (rawDataButton.isSelected) {
|
||||
TimePlot.put(this, rawValue)
|
||||
} else {
|
||||
TimePlot.put(this, temperature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun clearPlot() {
|
||||
plottables.clear()
|
||||
private fun getPlot(channelName: String): Plot? {
|
||||
return if (plotFrame.plots.has(channelName)) {
|
||||
plotFrame.get(channelName)
|
||||
} else {
|
||||
device.channels.values.find { it.name == channelName }?.let {
|
||||
TimePlot(it.name).apply {
|
||||
configure(it.meta())
|
||||
plotFrame.add(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearPlot() {
|
||||
plotFrame.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Alexander Nozik.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package inr.numass.control.cryotemp
|
||||
|
||||
/**
|
||||
* Created by darksnake on 28-Sep-16.
|
||||
*/
|
||||
data class PKT8Result(var channel: String, var rawValue: Double, var temperature: Double) {
|
||||
|
||||
val rawString: String = String.format("%.2f", rawValue)
|
||||
|
||||
val temperatureString: String = String.format("%.2f", temperature)
|
||||
}
|
Loading…
Reference in New Issue
Block a user