Compare commits
61 Commits
fed1b75fd2
...
develop_mh
| Author | SHA1 | Date | |
|---|---|---|---|
| a117d417f4 | |||
| 32842d4b6e | |||
| 703a279ffa | |||
| 0d388b8c42 | |||
| ba25fada8e | |||
| ac06cd281f | |||
| a98aab4f45 | |||
| 36bea152fd | |||
| 90f89e08b8 | |||
| 7a99cd1dda | |||
| 9badb4dcd1 | |||
| 05f610bd86 | |||
| 91aefdba24 | |||
| fc61bc87f6 | |||
| 02932e8b79 | |||
| f7cedfd799 | |||
| 07c5d0fa2a | |||
| 69c98034d6 | |||
| 2f237acaec | |||
| 55715a20eb | |||
| 29841e2d15 | |||
| faaf8677e3 | |||
| ec259c981c | |||
| d4d30f1982 | |||
| 60138964cf | |||
| 9ce0efdb8c | |||
| 0047ed9587 | |||
| d9ad314b62 | |||
| 40f51173aa | |||
| e53449e001 | |||
| bdd562f0dd | |||
| cda965cb81 | |||
| 77c840064c | |||
| 31663fb9df | |||
| cbf28071b0 | |||
| 760561b10e | |||
| 2434c5b7aa | |||
| 9291a5d1db | |||
| ddb0d6a699 | |||
| a9b0fe80e4 | |||
| fbf93e80ae | |||
| 95a126953b | |||
| 30aa429c76 | |||
| 423460df66 | |||
| cb19bc990d | |||
| a958915d41 | |||
| 9ca6e10ac1 | |||
| def8d28c3b | |||
| cf3dae63f3 | |||
| f7131c827f | |||
| 32d0ac69c9 | |||
| eb3bc33509 | |||
| cd7620672f | |||
| 4a3d0313a2 | |||
| 8041453c5e | |||
| 6209b926e2 | |||
| 09068d9f70 | |||
| a479f5da7c | |||
| d33c49d1e1 | |||
| 578f41c212 | |||
| f90962c54f |
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="development.sqlite3" uuid="d3ab71a5-ebb0-4573-9e48-bc4bd013b015">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/storage/development.sqlite3</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/dictionaries/rubyon.xml
generated
Normal file
7
.idea/dictionaries/rubyon.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="rubyon">
|
||||||
|
<words>
|
||||||
|
<w>farmitry</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
3
.idea/farmitry_hvac.iml
generated
3
.idea/farmitry_hvac.iml
generated
@@ -53,6 +53,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.6, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="ccutrer-serialport (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="erb_lint (v0.9.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="erb_lint (v0.9.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="foreman (v0.88.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="foreman (v0.88.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
@@ -129,6 +131,7 @@
|
|||||||
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.1.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
<orderEntry type="library" scope="PROVIDED" name="rufus-scheduler (v3.9.2, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.31.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.31.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
<orderEntry type="library" scope="PROVIDED" name="smart_properties (v1.17.0, rbenv: 3.4.2) [gem]" level="application" />
|
<orderEntry type="library" scope="PROVIDED" name="smart_properties (v1.17.0, rbenv: 3.4.2) [gem]" level="application" />
|
||||||
|
|||||||
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/app/controllers/schedules_controller.rb" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
Gemfile
1
Gemfile
@@ -71,3 +71,4 @@ end
|
|||||||
|
|
||||||
gem "ccutrer-serialport"
|
gem "ccutrer-serialport"
|
||||||
gem "rmodbus"
|
gem "rmodbus"
|
||||||
|
gem "rufus-scheduler"
|
||||||
|
|||||||
22
Gemfile.lock
22
Gemfile.lock
@@ -129,6 +129,8 @@ GEM
|
|||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
ffi (1.17.2-aarch64-linux-gnu)
|
ffi (1.17.2-aarch64-linux-gnu)
|
||||||
|
ffi (1.17.2-arm64-darwin)
|
||||||
|
ffi (1.17.2-x86_64-linux-gnu)
|
||||||
foreman (0.88.1)
|
foreman (0.88.1)
|
||||||
fugit (1.11.1)
|
fugit (1.11.1)
|
||||||
et-orbi (~> 1, >= 1.2.11)
|
et-orbi (~> 1, >= 1.2.11)
|
||||||
@@ -192,18 +194,10 @@ GEM
|
|||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.7-aarch64-linux-gnu)
|
nokogiri (1.18.7-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-aarch64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm-linux-gnu)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.7-arm64-darwin)
|
nokogiri (1.18.7-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-x86_64-linux-gnu)
|
nokogiri (1.18.7-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.7-x86_64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.8.0)
|
parser (3.3.8.0)
|
||||||
@@ -303,6 +297,8 @@ GEM
|
|||||||
rubocop-rails (>= 2.30)
|
rubocop-rails (>= 2.30)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
|
rufus-scheduler (3.9.2)
|
||||||
|
fugit (~> 1.1, >= 1.11.1)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.31.0)
|
selenium-webdriver (4.31.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
@@ -328,12 +324,8 @@ GEM
|
|||||||
railties (>= 7.1)
|
railties (>= 7.1)
|
||||||
thor (~> 1.3.1)
|
thor (~> 1.3.1)
|
||||||
sqlite3 (2.6.0-aarch64-linux-gnu)
|
sqlite3 (2.6.0-aarch64-linux-gnu)
|
||||||
sqlite3 (2.6.0-aarch64-linux-musl)
|
|
||||||
sqlite3 (2.6.0-arm-linux-gnu)
|
|
||||||
sqlite3 (2.6.0-arm-linux-musl)
|
|
||||||
sqlite3 (2.6.0-arm64-darwin)
|
sqlite3 (2.6.0-arm64-darwin)
|
||||||
sqlite3 (2.6.0-x86_64-linux-gnu)
|
sqlite3 (2.6.0-x86_64-linux-gnu)
|
||||||
sqlite3 (2.6.0-x86_64-linux-musl)
|
|
||||||
sshkit (1.24.0)
|
sshkit (1.24.0)
|
||||||
base64
|
base64
|
||||||
logger
|
logger
|
||||||
@@ -377,13 +369,8 @@ GEM
|
|||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
aarch64-linux-gnu
|
aarch64-linux-gnu
|
||||||
aarch64-linux-musl
|
|
||||||
arm-linux-gnu
|
|
||||||
arm-linux-musl
|
|
||||||
arm64-darwin-24
|
arm64-darwin-24
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
x86_64-linux-gnu
|
|
||||||
x86_64-linux-musl
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
amazing_print
|
amazing_print
|
||||||
@@ -403,6 +390,7 @@ DEPENDENCIES
|
|||||||
rails (~> 8.0.2)
|
rails (~> 8.0.2)
|
||||||
rmodbus
|
rmodbus
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
|
rufus-scheduler
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
|
|||||||
5
app/assets/images/svg/svg_logo.svg
Normal file
5
app/assets/images/svg/svg_logo.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="189" height="189" viewBox="0 0 189 189" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M187.296 112.455C178.909 156.06 140.551 189 94.5 189C42.8066 189 0.807589 147.494 0.0114746 95.99C3.71633 92.1456 7.55581 88.7053 11.4659 85.6265C11.1579 88.5422 11 91.5027 11 94.5C11 140.616 48.3842 178 94.5 178C126.824 178 154.858 159.633 168.736 132.765C176.362 127.258 182.483 120.223 187.296 112.455ZM188.892 89.9371C185.31 93.8915 181.585 97.4466 177.777 100.643C177.925 98.6143 178 96.5659 178 94.5C178 48.3843 140.616 11 94.5 11C63.5437 11 36.5219 27.8457 22.1015 52.8701C14.5028 57.4406 8.23356 63.4529 3.13478 70.2708C13.8339 29.8192 50.6846 0 94.5 0C145.161 0 186.511 39.8653 188.892 89.9371Z" fill="#158078"/>
|
||||||
|
<path d="M128 59C73.6 23 25 55.6667 7.5 76.5C7.5 76.5 44 50.0086 88.5 59C21 66 1.5 95.5 1.5 95.5L7.5 105.5L16 99.5C16 99.5 25 110 47.5 105.5C70 101 101.5 55 128 59Z" fill="#158078"/>
|
||||||
|
<path d="M60 128.621C114.4 164.621 163 131.954 180.5 111.121C180.5 111.121 144 137.613 99.5 128.621C167 121.621 186.5 92.1211 186.5 92.1211L180.5 82.1211L172 88.1211C172 88.1211 163 77.6211 140.5 82.1211C118 86.6211 86.5 132.621 60 128.621Z" fill="#158078"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -2,14 +2,21 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-default: rgb(226, 232, 240);
|
--color-default: rgb(226, 232, 240);
|
||||||
--color-defaultSlate: rgb(100, 116, 139);
|
--color-default-slate: rgb(100, 116, 139);
|
||||||
--color-defaultSlateDark: rgb(71, 85, 105);
|
--color-default-slate-dark: rgb(71, 85, 105);
|
||||||
--color-primary: rgb(66, 165, 245);
|
--color-primary: rgb(66, 165, 245);
|
||||||
--color-notice: rgb(19, 198, 154);
|
--color-notice: rgb(19, 198, 154);
|
||||||
--color-warning: rgb(255, 202, 40);
|
--color-warning: rgb(255, 202, 40);
|
||||||
--color-accept: rgb(121, 134, 203);
|
--color-accept: rgb(121, 134, 203);
|
||||||
--color-danger: rgb(239, 83, 80);
|
--color-danger: rgb(239, 83, 80);
|
||||||
--color-tableBorder: rgb(203, 213, 225);
|
--color-table-border: rgb(130, 144, 158);
|
||||||
|
|
||||||
|
/* theme */
|
||||||
|
--color-logo-color: rgb(21, 128, 120);
|
||||||
|
--color-base-background: rgb(39, 44, 56);
|
||||||
|
--color-base-secondary: rgb(55, 61, 71);
|
||||||
|
--color-base-text: rgb(255, 255, 255);
|
||||||
|
--color-base-border: rgb(77, 84, 102);
|
||||||
|
|
||||||
--base-font-size: 0.85rem;
|
--base-font-size: 0.85rem;
|
||||||
--text-xs: calc(var(--base-font-size) * 0.75);
|
--text-xs: calc(var(--base-font-size) * 0.75);
|
||||||
@@ -42,11 +49,21 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply text-sm text-defaultSlateDark
|
@apply bg-base-background text-base-text font-['Pretendard-Regular']
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-tableBorder
|
@apply border-table-border
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-pattern {
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
rgba(255, 255, 255, 0.01) 0,
|
||||||
|
rgba(255, 255, 255, 0.01) 24px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 36px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.disabled {
|
a.disabled {
|
||||||
@@ -58,33 +75,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply justify-center rounded-xs bg-defaultSlate px-2 py-0.5 text-sm font-semibold leading-6 text-white
|
@apply justify-center rounded-md bg-default-slate px-4 py-2 font-semibold leading-6 text-white
|
||||||
shadow-xs hover:opacity-80 cursor-pointer
|
shadow-xs hover:opacity-80 cursor-pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@apply bg-white border border-tableBorder rounded-xs shadow-xs
|
@apply bg-white border border-table-border rounded-xs shadow-xs
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Form*/
|
/*Form*/
|
||||||
.label-style {
|
.label-style {
|
||||||
@apply block my-1 font-bold text-defaultSlateDark
|
@apply block my-1 font-bold text-default-slate-dark
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-style {
|
.select-style {
|
||||||
@apply block w-full h-8 px-3 py-1.5 border border-defaultSlate/50 rounded-xs
|
@apply block w-full px-2 py-1.5 border border-default-slate/50 rounded-xs text-base-background appearance-none
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-style option {
|
||||||
|
color: theme('colors.base-background');
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-style {
|
.input-style {
|
||||||
@apply block w-full h-8 px-3 py-1.5 border border-defaultSlate/50 rounded-xs
|
@apply block w-full px-2 py-1.5 border border-default-slate/50 rounded-xs
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-style {
|
.textarea-style {
|
||||||
@apply block w-full pl-4 flex-1 py-1.5 ring-1 ring-inset ring-defaultSlate/50 rounded-xs
|
@apply block w-full pl-4 flex-1 py-1.5 ring-1 ring-inset ring-default-slate/50 rounded-xs
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Toggle Button*/
|
||||||
|
.toggle_style {
|
||||||
|
@apply w-14 h-8 bg-danger rounded-full peer-checked:bg-notice transition-all duration-300
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle_btn {
|
||||||
|
@apply absolute left-1 top-1 w-6 h-6 bg-white rounded-full transition-all duration-300 peer-checked:translate-x-full shadow-sm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.header {
|
||||||
|
@apply p-4 text-lg font-bold h-16 flex justify-between items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar {
|
||||||
|
/*@apply w-[16rem] pl-4 text-lg font-bold divide-y divide-base-border;*/
|
||||||
|
@apply w-[16rem] pl-4 pr-4 lg:pr-0 text-lg font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@apply lg:rounded-lg bg-base-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
@apply text-xl font-bold;
|
@apply text-xl font-bold;
|
||||||
}
|
}
|
||||||
@@ -94,7 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-title {
|
.form-title {
|
||||||
@apply text-center text-3xl font-bold tracking-tight text-defaultSlateDark
|
@apply text-center text-3xl font-bold tracking-tight text-default-slate-dark
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
@@ -107,15 +150,23 @@
|
|||||||
|
|
||||||
/*Menu*/
|
/*Menu*/
|
||||||
.menu-group {
|
.menu-group {
|
||||||
@apply py-2 px-4
|
@apply flex flex-row py-2 px-4 my-4 gap-x-4 justify-between items-center w-full
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group-title {
|
.menu-group-active {
|
||||||
@apply py-1 text-sm text-defaultSlateDark font-medium
|
@apply bg-base-secondary text-white rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group-items {
|
.menu-group:hover {
|
||||||
@apply p-1
|
@apply bg-base-secondary text-white transition-colors rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group-icon {
|
||||||
|
@apply py-1 text-sm font-medium justify-center
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group-name {
|
||||||
|
@apply p-1 flex-1
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-group-items-title {
|
.menu-group-items-title {
|
||||||
@@ -123,16 +174,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
.copyright {
|
||||||
@apply text-defaultSlateDark text-center
|
@apply text-default-slate-dark text-center
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Dashboard*/
|
||||||
|
.dashboard-group {
|
||||||
|
@apply rounded-md bg-base-background h-32 p-4 flex flex-col justify-between cursor-pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Pagy*/
|
/*Pagy*/
|
||||||
.pagy {
|
.pagy {
|
||||||
@apply flex items-center justify-center space-x-1 font-semibold text-sm text-defaultSlate;
|
@apply flex items-center justify-center space-x-1 font-semibold text-sm text-default-slate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagy a:not(.gap) {
|
.pagy a:not(.gap) {
|
||||||
@apply block rounded-xs px-2 py-1 border border-defaultSlate/50;
|
@apply block rounded-xs px-2 py-1 border border-default-slate/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagy a:not(.gap):hover {
|
.pagy a:not(.gap):hover {
|
||||||
@@ -140,11 +196,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pagy a:not(.gap):not([href]) {
|
.pagy a:not(.gap):not([href]) {
|
||||||
@apply text-defaultSlate/50 border border-defaultSlate/25 cursor-default
|
@apply text-default-slate/50 border border-default-slate/25 cursor-default
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagy a:not(.gap).current {
|
.pagy a:not(.gap).current {
|
||||||
@apply text-white bg-defaultSlate
|
@apply text-white bg-default-slate
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagy a.gap {
|
.pagy a.gap {
|
||||||
@@ -161,15 +217,15 @@
|
|||||||
|
|
||||||
/*Base Table*/
|
/*Base Table*/
|
||||||
.base-table {
|
.base-table {
|
||||||
@apply min-w-full divide-y divide-tableBorder whitespace-nowrap
|
@apply min-w-full whitespace-nowrap border border-table-border
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-table th {
|
.base-table th {
|
||||||
@apply bg-default px-2 py-2 text-left font-semibold text-defaultSlateDark lg:table-cell
|
@apply bg-base-background px-2 py-2 text-left font-bold lg:table-cell
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-table td {
|
.base-table td {
|
||||||
@apply border-t border-tableBorder px-2 py-1 text-defaultSlate lg:table-cell whitespace-nowrap
|
@apply border-t border-table-border px-2 py-1 lg:table-cell whitespace-nowrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +238,49 @@
|
|||||||
@apply text-lg font-bold;
|
@apply text-lg font-bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 기본 스크롤바 스타일 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(21, 21, 21, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: rgba(100, 100, 100, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-style::-webkit-outer-spin-button,
|
||||||
|
.input-style::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-style[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Fonts*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'GongGothicMedium';
|
||||||
|
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_20-10@1.0/GongGothicLight.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Pretendard-Regular';
|
||||||
|
src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||||
allow_browser versions: :modern
|
allow_browser versions: :modern
|
||||||
|
|
||||||
|
before_action :set_controllers
|
||||||
|
|
||||||
|
private
|
||||||
|
def set_controllers
|
||||||
|
@controllers = Controller.all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
2
app/controllers/device_controller.rb
Normal file
2
app/controllers/device_controller.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class DeviceController < ApplicationController
|
||||||
|
end
|
||||||
6
app/controllers/modals_controller.rb
Normal file
6
app/controllers/modals_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class ModalsController < ApplicationController
|
||||||
|
def open
|
||||||
|
@schedule = Schedule.new
|
||||||
|
render partial: "partials/modals_open", locals: { type: params[:type], id: params[:id], close_button: params[:close_button] }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,37 +1,2 @@
|
|||||||
class ModbusController < ApplicationController
|
class ModbusController < ApplicationController
|
||||||
def index
|
|
||||||
@schedule = Schedule.all
|
|
||||||
@modbus_running = Modbus::PollingService.running?
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
Modbus::PollingService.start
|
|
||||||
redirect_to modbus_index_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
Modbus::PollingService.stop
|
|
||||||
redirect_to modbus_index_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_edit
|
|
||||||
@schedule = Schedule.all
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_edit_update
|
|
||||||
error_hours = []
|
|
||||||
|
|
||||||
params[:schedule].each do |id, attributes|
|
|
||||||
schedule = Schedule.find_by(id: id)
|
|
||||||
unless schedule.update(temperature: attributes[:temperature])
|
|
||||||
error_hours << "#{schedule.hour}시"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if error_hours.any?
|
|
||||||
redirect_to modbus_index_path, alert: "#{error_hours.join(', ')}의 온도 업데이트에 실패하였습니다."
|
|
||||||
else
|
|
||||||
redirect_to modbus_index_path, notice: "스케줄이 성공적으로 업데이트되었습니다."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
80
app/controllers/schedules_controller.rb
Normal file
80
app/controllers/schedules_controller.rb
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
class SchedulesController < ApplicationController
|
||||||
|
def index
|
||||||
|
@controllers = Controller.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def view
|
||||||
|
@controller = Controller.find(params[:id])
|
||||||
|
@schedule = Schedule.where(controller_id: params[:id]).order(:hour, :minute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@schedule = Schedule.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@schedule = Schedule.new(schedule_params)
|
||||||
|
|
||||||
|
if @schedule.save
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), notice: "스케쥴이 추가 되었습니다."
|
||||||
|
else
|
||||||
|
error_messages = @schedule.errors.full_messages.join(", ")
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), alert: "스케쥴 추가 실패: #{error_messages}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@schedule = Schedule.find_by(id: params[:id])
|
||||||
|
|
||||||
|
if @schedule.destroy
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), notice: "스케줄이 삭제되었습니다."
|
||||||
|
else
|
||||||
|
error_messages = @schedule.errors.full_messages.join(", ")
|
||||||
|
redirect_to schedule_edit_schedule_path(@schedule.controller_id), alert: "스케쥴 삭제 실패: #{error_messages}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
Schedule.where(controller_id: params[:id]).destroy_all
|
||||||
|
ActiveRecord::Base.connection.execute("DELETE FROM sqlite_sequence WHERE name='schedules'")
|
||||||
|
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:id]), notice: "스케줄이 초기화되었습니다."
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_edit
|
||||||
|
@controller = Controller.find(params[:id])
|
||||||
|
@schedule = Schedule.where(controller_id: params[:id]).order(:hour, :minute)
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_edit_update
|
||||||
|
error_messages = []
|
||||||
|
|
||||||
|
params[:schedule]&.each do |id, attributes|
|
||||||
|
schedule = Schedule.find_by(id: id)
|
||||||
|
next unless schedule
|
||||||
|
|
||||||
|
unless schedule.update(
|
||||||
|
hour: attributes[:hour],
|
||||||
|
minute: attributes[:minute],
|
||||||
|
is_active: attributes[:is_active],
|
||||||
|
temperature: attributes[:temperature]
|
||||||
|
)
|
||||||
|
|
||||||
|
error_detail = schedule.errors.full_messages.join(", ")
|
||||||
|
time_label = "#{attributes[:hour]}시 #{attributes[:minute]}분"
|
||||||
|
error_messages << "#{time_label} - #{error_detail}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if error_messages.any?
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:controller_id]), alert: error_messages.join("<br>")
|
||||||
|
else
|
||||||
|
redirect_to schedule_edit_schedule_path(params[:controller_id]), notice: "스케줄이 성공적으로 업데이트되었습니다."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def schedule_params
|
||||||
|
params.require(:schedule).permit(:controller_id, :hour, :minute, :is_active, :temperature)
|
||||||
|
end
|
||||||
|
end
|
||||||
2
app/controllers/web_controller.rb
Normal file
2
app/controllers/web_controller.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class WebController < ApplicationController
|
||||||
|
end
|
||||||
2
app/helpers/device_helper.rb
Normal file
2
app/helpers/device_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module DeviceHelper
|
||||||
|
end
|
||||||
2
app/helpers/modals_helper.rb
Normal file
2
app/helpers/modals_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module ModalsHelper
|
||||||
|
end
|
||||||
2
app/helpers/schedules_helper.rb
Normal file
2
app/helpers/schedules_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module SchedulesHelper
|
||||||
|
end
|
||||||
2
app/helpers/web_helper.rb
Normal file
2
app/helpers/web_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
module WebHelper
|
||||||
|
end
|
||||||
@@ -6,3 +6,12 @@ import { application } from "./application"
|
|||||||
|
|
||||||
import HelloController from "./hello_controller"
|
import HelloController from "./hello_controller"
|
||||||
application.register("hello", HelloController)
|
application.register("hello", HelloController)
|
||||||
|
|
||||||
|
import ModalsController from "./modals_controller"
|
||||||
|
application.register("modals", ModalsController)
|
||||||
|
|
||||||
|
import SidebarController from "./sidebar_controller"
|
||||||
|
application.register("sidebar", SidebarController)
|
||||||
|
|
||||||
|
import TimerController from "./timer_controller"
|
||||||
|
application.register("timer", TimerController)
|
||||||
|
|||||||
52
app/javascript/controllers/modals_controller.js
Normal file
52
app/javascript/controllers/modals_controller.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { useClickOutside } from 'stimulus-use'
|
||||||
|
|
||||||
|
// Connects to data-controller="modals"
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
// useClickOutside(this)
|
||||||
|
this.boundHandleKeydown = this.handleKeydown.bind(this)
|
||||||
|
document.addEventListener("keydown", this.boundHandleKeydown)
|
||||||
|
window.addEventListener('modals:close', this.close.bind(this))
|
||||||
|
|
||||||
|
const panelElement = document.getElementById("modals-panel")
|
||||||
|
const backdropElement = document.getElementById("modals-backdrop")
|
||||||
|
|
||||||
|
if (panelElement && backdropElement) {
|
||||||
|
setTimeout(() => {
|
||||||
|
panelElement.classList.remove('opacity-0', 'translate-y-4', 'sm:scale-95')
|
||||||
|
panelElement.classList.add('opacity-100', 'translate-y-0', 'sm:scale-100')
|
||||||
|
backdropElement.classList.remove('opacity-0')
|
||||||
|
backdropElement.classList.add('opacity-75')
|
||||||
|
}, 10) // 애니메이션이 바로 시작되도록 약간의 지연 추가
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
document.removeEventListener('keydown', this.boundHandleKeydown)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(event) {
|
||||||
|
const panelElement = document.getElementById("modals-panel")
|
||||||
|
const backdropElement = document.getElementById("modals-backdrop")
|
||||||
|
|
||||||
|
if (panelElement && backdropElement) {
|
||||||
|
panelElement.classList.remove("opacity-100", "translate-y-0", "sm:scale-100")
|
||||||
|
panelElement.classList.add("opacity-0", "translate-y-4", "sm:scale-95")
|
||||||
|
|
||||||
|
backdropElement.classList.remove("opacity-75")
|
||||||
|
backdropElement.classList.add("opacity-0")
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const modalsFrame = document.querySelector("turbo-frame#modals")
|
||||||
|
modalsFrame.innerHTML = ""
|
||||||
|
}, 300) // 닫힘 애니메이션 시간과 동일하게 300ms 지연 후 모달 제거
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/javascript/controllers/sidebar_controller.js
Normal file
15
app/javascript/controllers/sidebar_controller.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["menu", "overlay"]
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.menuTarget.classList.toggle("-translate-x-full")
|
||||||
|
this.overlayTarget.classList.toggle("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.menuTarget.classList.add("-translate-x-full")
|
||||||
|
this.overlayTarget.classList.add("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/javascript/controllers/timer_controller.js
Normal file
21
app/javascript/controllers/timer_controller.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {Controller} from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
// Connects to data-controller="timer"
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["output"]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.updateTime()
|
||||||
|
this.interval = setInterval(() => this.updateTime(), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime() {
|
||||||
|
const now = new Date()
|
||||||
|
this.outputTarget.textContent = `${now.getFullYear()}년 ${String(now.getMonth() + 1).padStart(2, '0')}월 ${String(now.getDate()).padStart(2, '0')}일 ` +
|
||||||
|
`${String(now.getHours()).padStart(2, '0')}시 ${String(now.getMinutes()).padStart(2, '0')}분 ${String(now.getSeconds()).padStart(2, '0')}초`
|
||||||
|
}
|
||||||
|
}
|
||||||
3
app/models/controller.rb
Normal file
3
app/models/controller.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Controller < ApplicationRecord
|
||||||
|
has_many :schedules
|
||||||
|
end
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
class Schedule < ApplicationRecord
|
class Schedule < ApplicationRecord
|
||||||
|
belongs_to :controller
|
||||||
|
|
||||||
|
validates :hour, presence: { message: "시간을 입력하세요" }
|
||||||
|
validates :minute, presence: { message: "분을 입력하세요" }
|
||||||
validates :temperature,
|
validates :temperature,
|
||||||
presence: true,
|
presence: { message: "온도를 입력하세요" },
|
||||||
numericality: true,
|
numericality: { message: "온도는 숫자여야 합니다" },
|
||||||
format: { with: /\A\d+(\.\d)?\z/ }
|
format: { with: /\A\d+(\.\d)?\z/, message: "온도는 소수점 한 자리까지 입력 가능합니다" }
|
||||||
|
validates :minute, uniqueness: { scope: [ :controller_id, :hour ], message: "같은 시:분의 스케쥴이 이미 존재합니다" }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,47 +1,60 @@
|
|||||||
|
# polling_service.rb
|
||||||
|
require "rufus-scheduler"
|
||||||
|
|
||||||
module Modbus
|
module Modbus
|
||||||
class PollingService
|
class PollingService
|
||||||
class << self
|
class << self
|
||||||
def start
|
def start
|
||||||
return if $modbus_polling_threads.any?(&:alive?)
|
return if @scheduler
|
||||||
|
|
||||||
thread = Thread.new do
|
puts "[#{Time.now}] Modbus polling service 시작됨 (Rufus)"
|
||||||
puts "[#{Time.current}] Modbus polling service 시작됨"
|
@scheduler ||= Rufus::Scheduler.new
|
||||||
last_logged_hour = nil
|
|
||||||
loop do
|
@scheduler.cron "0 * * * * *" do
|
||||||
begin
|
|
||||||
now = Time.now
|
now = Time.now
|
||||||
current_hour = now.hour
|
current_time = format("%02d:%02d", now.hour, now.min)
|
||||||
|
puts "# current time: #{current_time}.#{now.sec}"
|
||||||
if current_hour != last_logged_hour && now.min == 0
|
schedules = Schedule.where(hour: now.hour, minute: now.min)
|
||||||
schedule = Schedule.find_by(hour: current_hour)
|
schedules.each { |schedule| apply_schedule(schedule) }
|
||||||
|
end
|
||||||
serial_path = Rails.root.join("serial.rb")
|
|
||||||
system("ruby", serial_path.to_s, "#{schedule.temperature * 10}")
|
|
||||||
|
|
||||||
puts "[Schedule] #{current_hour}:00 -> Target temp: #{schedule.temperature}°C"
|
|
||||||
last_logged_hour = current_hour
|
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
|
||||||
error_message = "[#{Time.current}] 오류: #{e.message}"
|
|
||||||
|
|
||||||
|
private
|
||||||
|
def apply_schedule(schedule)
|
||||||
|
if schedule.is_active
|
||||||
|
run_script(
|
||||||
|
"power",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
"0",
|
||||||
|
"[Schedule] #{schedule.controller.name} ON"
|
||||||
|
)
|
||||||
|
sleep 2
|
||||||
|
run_script(
|
||||||
|
"temp",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
(schedule.temperature * 10).to_i.to_s,
|
||||||
|
"[Schedule] #{schedule.controller.name} #{format('%02d:%02d', schedule.hour, schedule.minute)} → #{schedule.temperature}°C"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
run_script(
|
||||||
|
"power",
|
||||||
|
"#{schedule.controller.station_id}",
|
||||||
|
"1",
|
||||||
|
"[Schedule] #{schedule.controller.name} OFF"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_script(mode, controller_id, value, success_msg)
|
||||||
|
if system("ruby", "modbus.rb", mode, controller_id, value)
|
||||||
|
puts success_msg
|
||||||
|
Rails.logger.info success_msg
|
||||||
|
else
|
||||||
|
error_message = "[#{Time.now}] 실행 실패 (station_id: #{controller_id}, value: #{value})"
|
||||||
puts error_message
|
puts error_message
|
||||||
ensure
|
Rails.logger.error error_message
|
||||||
sleep 0.1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
$modbus_polling_threads << thread
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
$modbus_polling_threads.each { |t| t.kill if t.alive? }
|
|
||||||
$modbus_polling_threads.clear
|
|
||||||
puts "[#{Time.now}] Modbus polling service 중지됨"
|
|
||||||
end
|
|
||||||
|
|
||||||
def running?
|
|
||||||
$modbus_polling_threads.any?(&:alive?)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
1
app/views/device/index.html.erb
Normal file
1
app/views/device/index.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div class="p-4">DEVICE</div>
|
||||||
@@ -20,11 +20,31 @@
|
|||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="background-pattern min-h-screen min-w-full overflow-x-auto h-full whitespace-nowrap">
|
||||||
<main class="m-4">
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full" data-controller="sidebar">
|
||||||
|
<%= render "partials/header" %>
|
||||||
|
<div class="flex flex-row flex-1 w-full">
|
||||||
|
<%= render "partials/sidebar" %>
|
||||||
|
<div class="flex-1 h-full pb-0 px-0 lg:pb-4 lg:px-4">
|
||||||
|
<div class="flex-1 h-full content">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="rounded-tl-md rounded-tr-md px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="rounded-tl-md rounded-tr-md px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
50
app/views/layouts/device.html.erb
Normal file
50
app/views/layouts/device.html.erb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="h-screen overflow-hidden bg-base-background text-base-text min-w-[400px] overflow-x-auto">
|
||||||
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full">
|
||||||
|
<%= render "partials/header" %>
|
||||||
|
<div class="flex flex-row flex-1 w-full overflow-hidden">
|
||||||
|
<%= render "partials/sidebar" %>
|
||||||
|
<div class="flex-1 h-full pb-4 pr-4">
|
||||||
|
<div class="flex-1 h-full content">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
47
app/views/layouts/web.html.erb
Normal file
47
app/views/layouts/web.html.erb
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for(:title) || "Farmitry Hvac" %></title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
|
<%= yield :head %>
|
||||||
|
|
||||||
|
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
||||||
|
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
||||||
|
|
||||||
|
<link rel="icon" href="/icon.png" type="image/png">
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||||
|
<link rel="apple-touch-icon" href="/icon.png">
|
||||||
|
|
||||||
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<%= turbo_frame_tag :modals %>
|
||||||
|
<main class="flex flex-col h-full divide-y divide-border-table-border">
|
||||||
|
<%= render "partials/header" %>
|
||||||
|
<div class="flex flex-row flex-1 w-full divide-x divide-border-table-border">
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-accept text-white">
|
||||||
|
<%= raw flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% elsif flash[:alert] %>
|
||||||
|
<div class="m-4 rounded px-4 py-2 bg-danger text-white">
|
||||||
|
<%= raw flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<meta http-equiv="refresh" content="5">
|
|
||||||
|
|
||||||
<% if flash[:notice] %>
|
|
||||||
<div class="mb-4 rounded bg-green-100 px-4 py-2 text-green-800 border border-green-300">
|
|
||||||
<%= flash[:notice] %>
|
|
||||||
</div>
|
|
||||||
<% elsif flash[:alert] %>
|
|
||||||
<div class="mb-4 rounded bg-red-100 px-4 py-2 text-red-800 border border-red-300">
|
|
||||||
<%= flash[:alert] %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% @schedule.each do |s| %>
|
|
||||||
<div class="flex justify-items-center gap-x-2">
|
|
||||||
<div><%= s.hour %>시</div>
|
|
||||||
<div><%= s.temperature %> °C</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="py-4">
|
|
||||||
<%= button_to "수정", schedule_edit_modbus_index_path, method: :get, class: "btn bg-primary" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-x-2">
|
|
||||||
<%= button_to "스케쥴러 시작", start_modbus_index_path, method: :post, class: "btn bg-accept" %>
|
|
||||||
<%= button_to "스케쥴러 정지", stop_modbus_index_path, method: :post, class: "btn bg-danger" %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-4">
|
|
||||||
<span class="font-semibold">스케쥴러 상태:</span>
|
|
||||||
<% if @modbus_running %>
|
|
||||||
<span class="text-accept font-bold">실행 중</span>
|
|
||||||
<% else %>
|
|
||||||
<span class="text-danger font-bold">정지됨</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<%= form_with url: schedule_edit_update_modbus_index_path, method: :post do %>
|
|
||||||
<% @schedule.each do |s| %>
|
|
||||||
<div class="flex justify-items-center gap-x-2 pb-2">
|
|
||||||
<div><%= s.hour %>시</div>
|
|
||||||
<div>
|
|
||||||
<%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, step: "0.1", inputmode: "decimal", class: "border px-2 py-1 rounded" %> °C
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="pt-4">
|
|
||||||
<%= submit_tag "업데이트", class: "bg-blue-500 text-white px-4 py-2 rounded" %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
14
app/views/partials/_header.html.erb
Normal file
14
app/views/partials/_header.html.erb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<header class="flex header justify-between items-center" data-controller="timer">
|
||||||
|
<div class="flex flex-row h-full items-center">
|
||||||
|
<%= image_tag "svg/svg_logo.svg", class: "w-auto h-8 mr-4" %>
|
||||||
|
<%= link_to root_path do %>
|
||||||
|
<div class="flex h-full items-center tracking-[0.5rem]"><span class="text-logo-color">FARM</span>ITRY</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-timer-target="output" class="hidden lg:block"></div>
|
||||||
|
|
||||||
|
<button data-action="click->sidebar#toggle" class="block lg:hidden p-2 cursor-pointer">
|
||||||
|
<i class="fa-solid fa-bars text-2xl"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
44
app/views/partials/_modals_open.html.erb
Normal file
44
app/views/partials/_modals_open.html.erb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<%= turbo_frame_tag :modals do %>
|
||||||
|
<div class="relative z-50" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||||
|
<!--
|
||||||
|
Background backdrop, show/hide based on modal state.
|
||||||
|
|
||||||
|
Entering: "ease-out duration-300"
|
||||||
|
From: "opacity-0"
|
||||||
|
To: "opacity-100"
|
||||||
|
Leaving: "ease-in duration-200"
|
||||||
|
From: "opacity-100"
|
||||||
|
To: "opacity-0"
|
||||||
|
-->
|
||||||
|
<div id="modals-backdrop" class="fixed inset-0 bg-base-background transition-opacity opacity-0 ease-out duration-500" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-50 w-screen overflow-y-auto py-4">
|
||||||
|
<div class="flex min-h-full items-center justify-center text-center sm:items-center">
|
||||||
|
<!--
|
||||||
|
Modal panel, show/hide based on modal state.
|
||||||
|
|
||||||
|
Entering: "ease-out duration-300"
|
||||||
|
From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
To: "opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
Leaving: "ease-in duration-200"
|
||||||
|
From: "opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
-->
|
||||||
|
<div id="modals-panel" class="p-4 space-y-4 z-50 relative transform overflow-hidden rounded-md bg-base-secondary text-left shadow-xl transition-all w-full max-w-full mx-2 sm:mx-10 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95 ease-out duration-500" data-controller="modals" data-action="modals:click:outside->modals#close">
|
||||||
|
<div>
|
||||||
|
<%= render "partials/modals/#{type}" %>
|
||||||
|
</div>
|
||||||
|
<% if close_button != "false" %>
|
||||||
|
<%= button_tag type: 'button',
|
||||||
|
data: {
|
||||||
|
action: 'modals#close'
|
||||||
|
},
|
||||||
|
class: 'inline-flex btn w-full' do %>
|
||||||
|
<div class="items-center"><i class="fa-solid fa-square-caret-down"></i> 닫기</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
31
app/views/partials/_sidebar.html.erb
Normal file
31
app/views/partials/_sidebar.html.erb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<div data-sidebar-target="overlay" data-action="click->sidebar#close" class="fixed inset-0 bg-black/50 z-40 hidden lg:hidden"></div>
|
||||||
|
<nav class="side-bar overflow-y-auto transition-transform lg:translate-x-0 -translate-x-full bg-base-background lg:bg-transparent fixed lg:relative top-0 left-0 h-full z-40" data-sidebar-target="menu">
|
||||||
|
<%= link_to root_path, class: "menu-group #{'menu-group-active' if current_page?(root_path)}" do %>
|
||||||
|
<div class="menu-group-icon">
|
||||||
|
<i class="fa-solid fa-chart-line text-base-text text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-group-name">
|
||||||
|
현황
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<hr >
|
||||||
|
<% @controllers.each do |c| %>
|
||||||
|
<%= link_to view_schedule_path(c.id), class: "menu-group #{'menu-group-active' if current_page?(view_schedule_path(c.id))}" do %>
|
||||||
|
<div class="menu-group-icon">
|
||||||
|
<i class="fa-solid fa-temperature-high text-base-text text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-group-name">
|
||||||
|
<%= c.name %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<hr >
|
||||||
|
<% end %>
|
||||||
|
<div class="menu-group">
|
||||||
|
<div class="menu-group-icon">
|
||||||
|
<i class="fa-solid fa-gear text-base-text text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="menu-group-name">
|
||||||
|
설청
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
41
app/views/partials/modals/_add_schedule.html.erb
Normal file
41
app/views/partials/modals/_add_schedule.html.erb
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<%= form_with model: @schedule, method: :post, data: { turbo: false }, class: 'flex flex-col h-full space-y-4' do |form| %>
|
||||||
|
<%= form.hidden_field :controller_id, value: params[:id] %>
|
||||||
|
<div class="flex-1 text-white space-y-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= Controller.find(params[:id]).name %> 컨트롤러</div>
|
||||||
|
<table class="base-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>시간</th>
|
||||||
|
<th>분</th>
|
||||||
|
<th>ON/OFF</th>
|
||||||
|
<th>온도</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<%= form.select :hour,
|
||||||
|
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, @schedule.hour),
|
||||||
|
{},
|
||||||
|
class: "select-style text-white" %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.number_field :minute,
|
||||||
|
min: 0, max: 59, step: 1, inputmode: "decimal", class: "input-style" %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= check_box_tag "schedule[is_active]", "1", true %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.number_field :temperature,
|
||||||
|
step: "0.1", inputmode: "decimal", class: "input-style" %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<%= submit_tag "추가", class: "btn bg-primary" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
159
app/views/schedules/index.html.erb
Normal file
159
app/views/schedules/index.html.erb
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<div class="flex-1 grid grid-cols-2 lg:grid-cols-4 gap-4 p-4">
|
||||||
|
<!-- 실내온도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">실내온도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full">
|
||||||
|
<span class="text-4xl font-['GongGothicMedium'] font-light">15.5</span>
|
||||||
|
<span class="text-2xl">°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 실내습도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">실내습도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-droplet text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CO2 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div><span class="text-xl">CO</span><span class="text-sm">2</span></div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<div class="size-10 bg-base-text/80 rounded-full text-base-background flex flex-col justify-center">
|
||||||
|
<div class="flex flex-row justify-center items-baseline">
|
||||||
|
<span class="text-xl">CO</span><span class="text-sm">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="items-baseline text-right"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- Fan -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">FAN</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-fan text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 공조기 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">공조기</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">15.5</span>
|
||||||
|
<span class="text-2xl">°C</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LED -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">LED</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<i class="fa-solid fa-lightbulb text-base-text/80 text-4xl text-left w-full"></i>
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">60</span> <span class="text-2xl">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- pH -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-4xl">pH</div>
|
||||||
|
<div class="text-3xl font-['GongGothicMedium']">7.1</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-2xl">
|
||||||
|
6.0 ~ 6.0
|
||||||
|
</div>
|
||||||
|
<div><i class="fa-solid fa-gear text-base-text/30 text-2xl"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EC -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-4xl">EC</div>
|
||||||
|
<div class="text-3xl font-['GongGothicMedium']">0.1</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-baseline">
|
||||||
|
<div class="text-2xl">
|
||||||
|
0 ~ 1
|
||||||
|
</div>
|
||||||
|
<div><i class="fa-solid fa-gear text-base-text/30 text-2xl"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 양액온도 -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-between h-full">
|
||||||
|
<div class="text-xl">양액온도</div>
|
||||||
|
<div class="flex flex-row justify-between items-baseline w-full">
|
||||||
|
<!-- <i class="fa-solid fa-temperature-quarter text-base-text/80 text-4xl text-left w-full"></i>-->
|
||||||
|
<div class="items-baseline text-right w-full"><span class="text-4xl font-['GongGothicMedium']">20.96</span>
|
||||||
|
<span class="text-2xl">°C</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">1</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-recycle text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">자동화</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-rotate-right text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">초기화</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-circle-pause text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">긴급 정지</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
<div class="dashboard-group">
|
||||||
|
<div class="flex flex-col justify-center space-y-4 h-full">
|
||||||
|
<i class="fa-solid fa-faucet-drip text-base-text/80 text-5xl text-center w-full"></i>
|
||||||
|
<div class="text-xl text-center">1회 급수 설정</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
81
app/views/schedules/schedule_edit.html.erb
Normal file
81
app/views/schedules/schedule_edit.html.erb
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<div class="flex flex-col h-full text-white overflow-y-auto">
|
||||||
|
<div class="flex flex-col flex-1 divide-y divide-base-border">
|
||||||
|
<%= form_with url: schedule_edit_update_schedules_path, method: :post, class: 'flex flex-col h-full' do %>
|
||||||
|
<%= hidden_field_tag :controller_id, params[:id] %>
|
||||||
|
<div class="divide-y divide-base-border">
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= @controller.name %> 컨트롤러</div>
|
||||||
|
<%= submit_tag "업데이트", class: "btn bg-primary" %>
|
||||||
|
</div>
|
||||||
|
<ul class="divide-y divide-base-border whitespace-nowrap h-full relative">
|
||||||
|
<% if @schedule.present? %>
|
||||||
|
<% @schedule.each do |s| %>
|
||||||
|
<li class="flex justify-between items-center px-4 py-2 space-x-4 h-full relative">
|
||||||
|
<div class="flex items-center space-x-4 basis-3/5 h-full">
|
||||||
|
<!-- 시간 -->
|
||||||
|
<div class="flex flex-col lg:flex-row h-full items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0">
|
||||||
|
<div class="text-white text-left w-full lg:w-auto">시간</div>
|
||||||
|
<div class="text-xl flex items-center space-x-1 w-full h-full">
|
||||||
|
<%= select_tag "schedule[#{s.id}][hour]",
|
||||||
|
options_for_select((0..23).map { |h| [h.to_s.rjust(2, '0'), h] }, s.hour),
|
||||||
|
class: "select-style text-white min-w-[60px]" %>
|
||||||
|
<div>:</div>
|
||||||
|
<%= number_field_tag "schedule[#{s.id}][minute]",
|
||||||
|
s.minute, min: 0, max: 59, step: 1, inputmode: "decimal",
|
||||||
|
class: "input-style min-w-[60px]" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 온도 -->
|
||||||
|
<div class="flex flex-col lg:flex-row h-full items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0">
|
||||||
|
<div class="text-white text-left w-full lg:w-auto">온도 (°C)</div>
|
||||||
|
<div class="text-xl flex items-center">
|
||||||
|
<%= number_field_tag "schedule[#{s.id}][temperature]", s.temperature, min: -99, max: 99, step: "0.1", inputmode: "decimal", class: "input-style min-w-[68px]" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4 basis-2/5 justify-end h-full">
|
||||||
|
<!-- 사용 여부 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0 h-full">
|
||||||
|
<div class="text-white">ON/OFF</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer h-full">
|
||||||
|
<%= check_box_tag "schedule[#{s.id}][is_active]", "1", s.is_active == true || s.is_active == 1, class: "sr-only peer" %>
|
||||||
|
<div class="toggle_style"></div>
|
||||||
|
<div class="toggle_btn"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 삭제 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-center space-x-0 space-y-2 lg:space-x-4 lg:space-y-0 h-full relative">
|
||||||
|
<div class="text-white"> </div>
|
||||||
|
<%= link_to schedule_path(s),
|
||||||
|
data: { turbo_method: :delete, turbo_confirm: "정말 삭제하시겠습니까?" },
|
||||||
|
class: "btn bg-danger text-sm flex items-center px-2 py-0 my-1.5 lg:px-4 lg:py-1.5 lg:my-0" do %>
|
||||||
|
<div class="hidden lg:block">삭제</div>
|
||||||
|
<div class="block lg:hidden"><i class="fa-solid fa-xmark"></i></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<li class="text-center p-4 w-full">저장된 스케줄이 없습니다.</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="flex p-4 space-x-4">
|
||||||
|
<%= button_to "초기화", reset_schedule_path(params[:id]),
|
||||||
|
method: :post,
|
||||||
|
data: { turbo_confirm: "정말 초기화하시겠습니까? 모든 스케줄 데이터가 삭제됩니다." },
|
||||||
|
class: "btn bg-danger" %>
|
||||||
|
<%= button_to "추가하기", open_modals_path(type: "add_schedule", id: params[:id]),
|
||||||
|
class: "btn bg-default-slate",
|
||||||
|
data: {
|
||||||
|
turbo_method: :post,
|
||||||
|
turbo_frame: "modals"
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
74
app/views/schedules/view.html.erb
Normal file
74
app/views/schedules/view.html.erb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<div class="flex flex-col h-full divide-y divide-base-border text-white">
|
||||||
|
<div class="flex flex-col flex-1 space-y-4 overflow-y-hidden">
|
||||||
|
<div class="overflow-y-auto divide-y divide-base-border">
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<div class="text-2xl font-bold leading-10"><%= @controller.name %> 컨트롤러</div>
|
||||||
|
<%= link_to "수정", schedule_edit_schedule_path(@controller.id), class: "btn bg-default-slate" %>
|
||||||
|
</div>
|
||||||
|
<ul class="divide-y divide-base-border">
|
||||||
|
<% if @schedule.present? %>
|
||||||
|
<% @schedule.each do |s| %>
|
||||||
|
<li class="flex justify-between items-center px-4 py-2 space-x-4">
|
||||||
|
<div class="flex items-center space-x-6">
|
||||||
|
<!-- 시간 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-2">
|
||||||
|
<div class="text-white">시간</div>
|
||||||
|
<div class="text-3xl flex items-baseline space-x-1">
|
||||||
|
<div class="text-center w-[36px]"><%= s.hour.to_s.rjust(2, '0') %></div>
|
||||||
|
<div>:</div>
|
||||||
|
<div class="text-center w-[36px]"><%= s.minute.to_s.rjust(2, '0') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 온도 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-4">
|
||||||
|
<div class="text-white">온도</div>
|
||||||
|
<div class="text-3xl flex items-end space-x-2">
|
||||||
|
<div class="text-left w-[50px]"><%= s.temperature %></div>
|
||||||
|
<div class="text-white text-base leading-7">°C</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사용 여부 -->
|
||||||
|
<div class="flex flex-col lg:flex-row items-baseline space-x-0 lg:space-x-4 items-end">
|
||||||
|
<div class="text-white mb-1">ON/OFF</div>
|
||||||
|
<div class="text-3xl w-8 mr-0">
|
||||||
|
<%#= s.is_active %>
|
||||||
|
<% if s.is_active %>
|
||||||
|
<i class="fa-solid fa-check text-notice"></i>
|
||||||
|
<% else %>
|
||||||
|
<i class="fa-solid fa-xmark text-danger"></i>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<li class="text-center p-4 w-full">저장된 스케줄이 없습니다.</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- <table class="base-table">-->
|
||||||
|
<!-- <thead>-->
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <th>시간</th>-->
|
||||||
|
<!-- <th>분</th>-->
|
||||||
|
<!-- <th>사용여부</th>-->
|
||||||
|
<!-- <th>온도</th>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- </thead>-->
|
||||||
|
<!-- <tbody>-->
|
||||||
|
<%# @schedule.each do |s| %>
|
||||||
|
<!-- <tr>-->
|
||||||
|
<!-- <td><%#= s.hour %>시</td>-->
|
||||||
|
<!-- <td><%#= s.minute %>분</td>-->
|
||||||
|
<!-- <td><%#= s.is_active %></td>-->
|
||||||
|
<!-- <td><%#= s.temperature %> °C</td>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<%# end %>
|
||||||
|
<!-- </tbody>-->
|
||||||
|
<!-- </table>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
app/views/web/index.html.erb
Normal file
1
app/views/web/index.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div>WEB</div>
|
||||||
@@ -28,5 +28,7 @@ module FarmitryHvac
|
|||||||
#
|
#
|
||||||
# config.time_zone = "Central Time (US & Canada)"
|
# config.time_zone = "Central Time (US & Canada)"
|
||||||
# config.eager_load_paths << Rails.root.join("extras")
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
|
|
||||||
|
config.time_zone = "Asia/Seoul"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,14 +10,26 @@ Rails.application.routes.draw do
|
|||||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||||
|
|
||||||
# Defines the root path route ("/")
|
# Defines the root path route ("/")
|
||||||
root "modbus#index"
|
|
||||||
|
|
||||||
resources :modbus do
|
root "schedules#index"
|
||||||
collection do
|
|
||||||
post "start"
|
resources :schedules do
|
||||||
post "stop"
|
member do
|
||||||
|
get "view"
|
||||||
get "schedule_edit"
|
get "schedule_edit"
|
||||||
|
post "reset"
|
||||||
|
end
|
||||||
|
collection do
|
||||||
post "schedule_edit_update"
|
post "schedule_edit_update"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :modals do
|
||||||
|
collection do
|
||||||
|
post :open
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :device
|
||||||
|
resources :web
|
||||||
end
|
end
|
||||||
|
|||||||
10
db/migrate/20250416131430_create_controllers.rb
Normal file
10
db/migrate/20250416131430_create_controllers.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateControllers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :controllers do |t|
|
||||||
|
t.string :name
|
||||||
|
t.integer :station_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
class CreateSchedules < ActiveRecord::Migration[8.0]
|
class CreateSchedules < ActiveRecord::Migration[8.0]
|
||||||
def change
|
def change
|
||||||
create_table :schedules do |t|
|
create_table :schedules do |t|
|
||||||
|
t.references :controller, null: false, foreign_key: true
|
||||||
t.integer :hour
|
t.integer :hour
|
||||||
|
t.integer :minute
|
||||||
|
t.boolean :is_active
|
||||||
t.float :temperature
|
t.float :temperature
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
add_index :schedules, [ :controller_id, :hour, :minute ], unique: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
20
db/schema.rb
generated
20
db/schema.rb
generated
@@ -11,10 +11,24 @@
|
|||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_04_16_131440) do
|
ActiveRecord::Schema[8.0].define(version: 2025_04_16_131440) do
|
||||||
create_table "schedules", force: :cascade do |t|
|
create_table "controllers", force: :cascade do |t|
|
||||||
t.integer "hour"
|
t.string "name"
|
||||||
t.float "temperature"
|
t.integer "station_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "schedules", force: :cascade do |t|
|
||||||
|
t.integer "controller_id", null: false
|
||||||
|
t.integer "hour"
|
||||||
|
t.integer "minute"
|
||||||
|
t.boolean "is_active"
|
||||||
|
t.float "temperature"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["controller_id", "hour", "minute"], name: "index_schedules_on_controller_id_and_hour_and_minute", unique: true
|
||||||
|
t.index ["controller_id"], name: "index_schedules_on_controller_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key "schedules", "controllers"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
# MovieGenre.find_or_create_by!(name: genre_name)
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||||
# end
|
# end
|
||||||
|
|
||||||
|
Controller.create(name: "3번방", station_id: 7)
|
||||||
|
Controller.create(name: "2번방", station_id: 8)
|
||||||
|
|
||||||
(0..23).each do | h |
|
(0..23).each do | h |
|
||||||
Schedule.create!(hour: h, temperature: 15.0)
|
Schedule.create!(controller_id: 1, hour: h, minute: 0, is_active: true, temperature: 15.0)
|
||||||
|
Schedule.create!(controller_id: 2, hour: h, minute: 0, is_active: true, temperature: 15.0)
|
||||||
end
|
end
|
||||||
|
|||||||
34
modbus.rb
Normal file
34
modbus.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
require "rmodbus"
|
||||||
|
require "ccutrer-serialport"
|
||||||
|
require './sms/sms'
|
||||||
|
|
||||||
|
mode = ARGV[0]&.strip
|
||||||
|
controller_id = ARGV[1]&.to_i
|
||||||
|
value = ARGV[2]&.to_i
|
||||||
|
|
||||||
|
begin
|
||||||
|
ModBus::RTUClient.new("/dev/ttyUSB0", 9600) do |cl|
|
||||||
|
cl.with_slave(controller_id) do |slave|
|
||||||
|
regs = slave.holding_registers
|
||||||
|
case mode
|
||||||
|
when "temp"
|
||||||
|
regs[2] = value
|
||||||
|
when "power"
|
||||||
|
regs[22] = value
|
||||||
|
else
|
||||||
|
# type code here
|
||||||
|
end
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
error_message = "[#{Time.now}] #{mode} 실행 실패 (station_id: #{controller_id}, value: #{value})"
|
||||||
|
res = Sms.send_one(
|
||||||
|
{
|
||||||
|
to: '01062619801',
|
||||||
|
from: '01062619801',
|
||||||
|
text: error_message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
puts res
|
||||||
|
end
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"@hotwired/stimulus": "^3.2.2",
|
"@hotwired/stimulus": "^3.2.2",
|
||||||
"@hotwired/turbo-rails": "^8.0.13",
|
"@hotwired/turbo-rails": "^8.0.13",
|
||||||
"@tailwindcss/cli": "^4.1.4",
|
"@tailwindcss/cli": "^4.1.4",
|
||||||
"tailwindcss": "^4.1.4"
|
"tailwindcss": "^4.1.4",
|
||||||
|
"stimulus-use": "^0.52.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
sms/config.json
Executable file
7
sms/config.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"api_key": "NCSK1QGPTBWMUKBO",
|
||||||
|
"api_secret": "RSGJ20CGUOTRKK33RFONFUFHY2SWMU9X",
|
||||||
|
"protocol": "https",
|
||||||
|
"domain": "api.coolsms.co.kr",
|
||||||
|
"prefix": ""
|
||||||
|
}
|
||||||
84
sms/request.rb
Executable file
84
sms/request.rb
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
require 'openssl'
|
||||||
|
require 'base64'
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
|
module Request
|
||||||
|
@file = File.read File.join(File.dirname(__FILE__), './config.json')
|
||||||
|
@config = JSON.parse(@file)
|
||||||
|
|
||||||
|
def self.get_uri(path)
|
||||||
|
str = @config['protocol'] + '://' + @config['domain'] + @config['prefix']
|
||||||
|
puts str + path
|
||||||
|
uri = URI(str + path)
|
||||||
|
uri
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.header_get
|
||||||
|
api_key = @config['api_key']
|
||||||
|
api_secret = @config['api_secret']
|
||||||
|
date = Time.now.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
|
||||||
|
salt = SecureRandom.hex
|
||||||
|
signature = OpenSSL::HMAC.hexdigest('SHA256', api_secret, date + salt)
|
||||||
|
'HMAC-SHA256 apiKey=' + api_key + ', date=' + date + ', salt=' + salt + ', signature=' + signature
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.post(path, body)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.put(path, body, headers = nil)
|
||||||
|
auth = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Put.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', auth)
|
||||||
|
if headers
|
||||||
|
headers.each do |k, v|
|
||||||
|
req.add_field(k, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get(path)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Get.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
res = http.request(req)
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete(path, body)
|
||||||
|
header = header_get
|
||||||
|
uri = get_uri(path)
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
req = Net::HTTP::Delete.new(uri.path, 'Content-Type' => 'application/json')
|
||||||
|
req.add_field('Authorization', header)
|
||||||
|
res = http.request(req)
|
||||||
|
if body
|
||||||
|
req.body = body.to_json
|
||||||
|
end
|
||||||
|
JSON.parse(res.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
11
sms/sms.rb
Executable file
11
sms/sms.rb
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
require_relative "request"
|
||||||
|
|
||||||
|
module Sms
|
||||||
|
def self.send_one(message)
|
||||||
|
Request.post("/messages/v4/send", { message: message })
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.send_many(messages)
|
||||||
|
Request.post("/messages/v4/send-many", { messages: messages })
|
||||||
|
end
|
||||||
|
end
|
||||||
7
test/controllers/device_controller_test.rb
Normal file
7
test/controllers/device_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DeviceControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/modals_controller_test.rb
Normal file
7
test/controllers/modals_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ModalsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/schedules_controller_test.rb
Normal file
7
test/controllers/schedules_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class SchedulesControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
7
test/controllers/web_controller_test.rb
Normal file
7
test/controllers/web_controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class WebControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
11
test/fixtures/controllers.yml
vendored
Normal file
11
test/fixtures/controllers.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
# This model initially had no columns defined. If you add columns to the
|
||||||
|
# model remove the "{}" from the fixture names and add the columns immediately
|
||||||
|
# below each fixture, per the syntax in the comments below
|
||||||
|
#
|
||||||
|
one: {}
|
||||||
|
# column: value
|
||||||
|
#
|
||||||
|
two: {}
|
||||||
|
# column: value
|
||||||
7
test/models/controller_test.rb
Normal file
7
test/models/controller_test.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ControllerTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
||||||
@@ -571,6 +571,11 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
stimulus-use@^0.52.2:
|
||||||
|
version "0.52.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.52.3.tgz#d6f35fa93277274957a2ed98a7b04b4d702cb1d6"
|
||||||
|
integrity sha512-stZ5dID6FUrGCR/ChWUa0FT5Z8iqkzT6lputOAb50eF+Ayg7RzJj4U/HoRlp2NV333QfvoRidru9HLbom4hZVw==
|
||||||
|
|
||||||
tailwindcss@4.1.4, tailwindcss@^4.1.4:
|
tailwindcss@4.1.4, tailwindcss@^4.1.4:
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.4.tgz#27b3c910c6f1a47f4540451f3faf7cdd6d977a69"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.4.tgz#27b3c910c6f1a47f4540451f3faf7cdd6d977a69"
|
||||||
|
|||||||
Reference in New Issue
Block a user