FLEXSCHE

スタッフブログStaff Blog

入社のご報告&FLEXSCHEをGoogleカレンダーと連携してみた

2025/08/13
written by トネガワ

トネガワ

自己紹介

初めまして、2025年5月に開発として入社したトネガワと申します。

現在入社から約3カ月が過ぎたところで、すでに展示会に参加させていただいたり、メーリングリストで対応させていただいております。至らぬ点も多々あるかと思いますが、どうぞよろしくお願いいたします。

好きなアルゴリズムはKaratsuba法です。基本的な式変形や発想の転換で計算時間が数十倍、数百倍も変わると気持ちが良いですね。

Googleカレンダーと連携

アドインの作り方

FLEXSCHEは標準機能の汎用性の高さがウリですが、新たに自分で機能を作ることもできます。 自分で機能を作る方法には「アクション」や「アドイン」などがあり、C++やPythonなどでコードを書いてより複雑な機能を実装できるのがアドインです。

まずは簡単なアドインを作ってみます。

アドイン作成.png

スクリプト雛形生成ツールからアドインを作成します。ここでアドイン名、言語、実行方法を指定します。 アドインの実行方法はメニューバーから選んで使う、一定時間ごとに実行、作業を動かしたときに実行など多種多様です。

import clr
clr.AddReference('FLEXSCHE.Interop.AIM')
clr.AddReference('FLEXSCHE.Interop.SData')
clr.AddReference('FLEXSCHE.Interop.GUI')
clr.AddReference('FLEXSCHE.Interop.SDLib')
clr.AddReference('FLEXSCHE.Interop.FSEditor')
clr.AddReference('FLEXSCHE.Interop.GPEns')
clr.AddReference('FLEXSCHE.Net')
clr.AddReference('PyLib')
from FLEXSCHE.Interop import AIM, SData, GUI, SDLib, FSEditor, GPEns
from FLEXSCHE.XmlHelper import *
from PyLib import Script

def SelfRegistration(addIns: AIM.AddIns):
  ## この部分を変更した場合は「アドイン再登録」を実行してください
  addin = addIns.AddScript("movelog", int(GUI.AddInKeyType.AddInKeyHookAfterMovingOperation))


def movelog(keyEntity: AIM.KeyEntity):
  ## 不要な行は削除してください
  operRec = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDOperationRec)), SData.SDOperationRec)
  timeChart = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDTimeChart)), GUI.TimeChart)
  movingOperationInfo = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDMovingOperationInfo)), GUI.MovingOperationInfo)
  ## ここにコードを書いてください

  project = timeChart.Project
  dataspace: SData.SDSpace = Script.Cast(project.DataSpace, SData.ISDSpace)
  utility: SDLib.SDLUtility = Script.Cast(dataspace.SDLUtility, SDLib.ISDLUtility)
  resBefore = Script.Cast(movingOperationInfo.ResourceBefore, SData.SDResourceRec).Code
  resAfter = Script.Cast(movingOperationInfo.ResourceAfter, SData.SDResourceRec).Code
  startTime = utility.TimeToString(operRec.StartTime)
  endTime = utility.TimeToString(operRec.EndTime)
  S = f'{operRec.Code}: {resBefore}->{resAfter} {startTime} ~ {endTime} に変更されました'
  project.Panels.MessagePanel.AddLine('general', S)

  return True


def alert(msg: str):
  Script.ShowMessageBox(msg, "FLEXSCHE", AIM.MessageBoxType.MBTOk)

# モジュール名、アドイン名を"movelog"として登録
# Pythonを使う場合はアドイン設定からPythonのDLLを登録する必要があります

雛形生成ツールでOKを押すと上記のようなテンプレートが作成されます。 26行目~34行目の部分を書き足すと、作業を動かしたときに変更ログを出力するアドインができました。

スクリーンショット 2025-08-12 174556.png

Googleカレンダーと連携するアドインを作ろう!

休暇や会議の予定をGoogleカレンダーで管理していて、それを考慮して製造計画を立てたいといったシチュエーションは割とあるのではないでしょうか。

これはツールを使って
Googleカレンダー → エクセルファイル → CSVファイル → EDIF取り込み
としても実現できますが、取り込むたびに操作する項目が多く、できる操作も限定的です。

そこで、アドインでFLEXSCHE Components(APIのようなもの)とGoogleカレンダーAPIを使うと両者が直接やりとりできるようになり、より簡単に複雑な操作を実現できる!
というのがこの記事の本題になります。

それでは、早速作っていきましょう!
まずはGoogleカレンダー上の予定を自由カレンダーとして取り込む機能を実装したいと思います。


API周りの設定

APIの設定方法はGoogle Workspaceのチュートリアルに詳しく書かれており、基本的にこれを参考にすれば大丈夫です。

次に、資源ごとにカレンダーを作り、資源名 → カレンダーid の辞書を作っておきます。
カレンダーidはAPIの呼び出しに使います。

{
  "作業員1" : "11111111111111111111111@group.calendar.google.com",
  "作業員2" : "23232323232323232323232@group.calendar.google.com",
  "作業員3" : "aaaaaaaaaaaaaaaaaaaaaaa@group.calendar.google.com",
  "作業員4" : "sisisisisisisisisisisii@group.calendar.google.com"
}

// calendar.json
// カレンダーidはGoogleカレンダーの設定から確認できます

アドインの実装

まずはライブラリ部分を作りました。(長いため折りたたんでいます)
大まかに以下のようなことができます。

  • カレンダーの予定を取得
  • カレンダーの予定を追加
    • 通知の時間や個数を設定
    • 作業の色を属性(オーダー、工程、資源など)に合わせて指定
  • カレンダーの予定を削除
  • FLEXSCHEに自由カレンダーを登録

ライブラリ部分(クリックで展開)

# CalendarAPI.py
# FLEXSCHEアドイン用
import clr
clr.AddReference('FLEXSCHE.Interop.AIM')
clr.AddReference('FLEXSCHE.Interop.SData')
clr.AddReference('FLEXSCHE.Interop.GUI')
clr.AddReference('FLEXSCHE.Interop.SDLib')
clr.AddReference('FLEXSCHE.Interop.FSEditor')
clr.AddReference('FLEXSCHE.Interop.GPEns')
clr.AddReference('FLEXSCHE.Net')
clr.AddReference('PyLib')
from FLEXSCHE.Interop import AIM, SData, GUI, SDLib, FSEditor, GPEns
from FLEXSCHE.XmlHelper import *
from PyLib import Script
# googleカレンダーapi
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# インポート
import datetime
import os.path
import inspect
import time
import datetime
import json

# ------------------------ パラメータ ------------------------
# スコープ. アクセス権を指定できる(読み込み、書き込みの許可)
# 変えたらtoken.jsonを消す
SCOPES = ["https://www.googleapis.com/auth/calendar"]

# クエリごとのsleep時間(秒, 小数可)
# apiを大量に呼ばないようにするため
WAITING_TIME = 0.5 

# トークン、カレンダーid等
# token, credentials, calendarをコードと同じ階層に置く
token_path = os.path.join(os.path.dirname(__file__), "token.json")
creds_path = os.path.join(os.path.dirname(__file__), "credentials.json")
calendar_path = os.path.join(os.path.dirname(__file__), "calendar.json")
__Calendars = {}

# タイムゾーンの設定
# 東京の場合
# 'dateTime': '2025-05-28T09:00:00+09:00', 末尾に+9:00を追加する(offset_timezone)
# 'timeZone': 'Asia/Tokyo',
timezone = "Asia/Tokyo"
offset_timezone = "+09:00"
# -----------------------------------------------------------

class FLEXSCHEData:
  def __init__(self, keyEntity: AIM.KeyEntity) -> None:
    self.env = Script.Cast(keyEntity.get_ParamObject(int(GUI.ParamIDType.ParamIDEnvironment)), GUI.Environment)
    self.project: GUI.Project = self.env.Project
    self.ds: SData.SDSpace = Script.Cast(self.project.DataSpace, SData.ISDSpace)
    self.ut: SDLib.SDLUtility = Script.Cast(self.ds.SDLUtility, SDLib.ISDLUtility)

# デバッグ用
def alert(msg: any) -> None:
  Script.ShowMessageBox(str(msg), "FLEXSCHE", AIM.MessageBoxType.MBTOk)

# カレンダーid(json)を読み込む
def InitCalendar(is_first: bool = True) -> None:
  if is_first:
    global __Calendars
    if calendar_path != '':
      with open(calendar_path, encoding="utf-8_sig") as f:
        __Calendars = json.load(f)
    is_first = False

# recourcecodeのカレンダーidを取得(なければ空文字列)
def GetCalendarId(rescode: str) -> str:
  InitCalendar()
  if rescode in __Calendars:
    return __Calendars[rescode]
  else:
    return ""

# calendaridのresidを取得(複数ある場合初めに見つけたやつを返す. なければ空文字列)
def GetResId(calendarid: str) -> str:
  InitCalendar()
  for k, v in __Calendars.items():
    if v == calendarid:
      return k
  return ""

# カレンダー(の参照)を取得
def GetAllCalendars() -> map:
  InitCalendar()
  return __Calendars

# tokenをチェックしてcredsを返す
def CheckToken():
  creds = None
  if os.path.exists(token_path):
    creds = Credentials.from_authorized_user_file(token_path, SCOPES)
  if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
      creds.refresh(Request())
    else:
      flow = InstalledAppFlow.from_client_secrets_file(creds_path, SCOPES)
      creds = flow.run_local_server(port=0)
    with open(token_path, "w") as token:
      token.write(creds.to_json())
  time.sleep(WAITING_TIME)
  return creds

# 20250528T090000 -> 2025-05-28T09:00:00
def InsertHyphen(s: str) -> str:
  return s[0:4] + '-' + s[4:6] + '-' + s[6:8] + 'T' + s[9:11] + ':' + s[11:13] + ':00'

# 2025-05-28T09:00:00 -> 20250528T090000
def DeleteHyphen(s: str) -> str:
  return s[0:4] + s[5:7] + s[8:13] + s[14:16]

# FLEXSCHE時間 -> GoogleCalendar時間
def ToGoogleTime(data: FLEXSCHEData, T: float) -> str:
  S = str(data.ut.TimeToISO8601(T))
  assert len(S) != 0
  if len(S) < 13:
    S += 'T0000'
  return InsertHyphen(S) + offset_timezone

# GoogleCalendar時間 -> FLEXSCHE時間
def ToFlexscheTime(data: FLEXSCHEData, T: str) -> float:
  return data.ut.TimeFromISO8601(DeleteHyphen(T))

# 全てのイベントを自由カレンダーとして取り込む (成功したか返す)
def ImportSchedulesAsFreeCalendars(data: FLEXSCHEData, calendarid: int) -> bool:
  creds = CheckToken()
  try:
    service = build("calendar", "v3", credentials=creds)
    events = service.events().list(calendarId=calendarid).execute()["items"]
    rescode = GetResId(calendarid)
    res = data.ds.ResourceSet.get_ResourceRecByCode(rescode)
    for event in events:
      Ts = event['start']['dateTime'] if 'dateTime' in event['start'] else event['start']['date'] + 'T00:00:00' + offset_timezone
      Ts = ToFlexscheTime(data, Ts)
      Te = event['end']['dateTime'] if 'dateTime' in event['end'] else event['end']['date'] + 'T00:00:00' + offset_timezone
      Te = ToFlexscheTime(data, Te)
      rr = data.ds.FreeCalendarSet.CreateFreeCalendarRec(SData.SDFreeCalendarType.SDFCTypeAssociatedToTimeSeries, True)
      rr.set_StartTime(Ts)
      rr.set_Duration(Te - Ts)
      rr.ResourceRec = res
      rr.set_Comment('name', event['summary'] if 'summary' in event else '無題のイベント')
      time.sleep(WAITING_TIME)
    return True
  except HttpError as error:
    return False

# color = {0:資源ごと, 1:オーダーごと, 2:工程ごと}
def GetColor(op: SData.SDataType.SDTypeOperation, res: SData.SDataType.SDTypeResource, color: int) -> int:
  ans = -1
  if color == 0:
    ans = hash(res.Code)
  elif color == 1:
    ans = hash(op.SingleOrderRec.Code)
  elif color == 2:
    ans = hash(op.Specifier)
  else:
    ans = 0 # colorが無効な値なら資源ごとにする
  return (ans + 2) % 12

# リスト中の作業を全て追加(成功したか返す)
def AddAllEvents(data: FLEXSCHEData, ops: list, color: int = 0) -> bool:
  creds = CheckToken()
  try:
    service = build("calendar", "v3", credentials=creds)
    for op in ops:
      for i in range(op.CountOfTaskRecs):
        task = op.get_TaskRec(i)
        if task.IsAssigned == 0:
          continue
        Ts = ToGoogleTime(data, task.AssignmentStartTime)
        Te = ToGoogleTime(data, task.AssignmentEndTime)

        # direction = {0:作業追加時から未来方向に, 1:作業開始前から過去方向に}
        # offset(>=0)分動かした時刻に通知  
        def AddReminder(event: map, direction: bool, offset: int) -> None:
          assert offset >= 0
          if direction == False:
            Tn = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9)))
            Sdiff = (datetime.datetime.fromisoformat(Ts) - Tn).total_seconds() - offset * 60
            # 過去には通知しない
            if Sdiff < 0:
              return
            event['reminders']['overrides'].append({
              'method': 'popup',
              'minutes': Sdiff // 60
            })
          else:
            event['reminders']['overrides'].append({
              "method": 'popup',
              "minutes": offset
            })

        # 場所を設定
        def SetPlace(event: map, place: str) -> None:
          event['location'] = place

        # 説明を設定
        def SetDetail(event: map, detail: str) -> None:
          event['description'] = detail

        event = {
          'summary': op.Code,
          'start': {
            'dateTime': Ts,
            'timeZone': timezone,
          },
          'end': {
            'dateTime': Te,
            'timeZone': timezone,
          },
          'colorId': GetColor(op, task.AssignedResourceRec, color),
          'reminders': {
            'useDefault': False,
            'overrides': []
          }
        }
        AddReminder(event, False, 0) # 作業登録時に通知
        AddReminder(event, True, 1) # 作業開始1分前に通知
        SetPlace(event, op.ProcRec.get_Comment('場所')) # 工程マスタのコメント:場所を登録
        SetDetail(event, op.ProcRec.get_Comment('説明')) # 工程マスタのコメント:場所を登録
        event = service.events().insert(calendarId=GetCalendarId(task.AssignedResourceRec.Code), body=event).execute()
        time.sleep(WAITING_TIME)
    return True
  except HttpError as error:
    return False

# 指定した作業を追加(成功したか返す)
def AddEvent(data: FLEXSCHEData, op: SData.SDataType.SDTypeOperation, color: int = 0) -> bool:
  return AddAllEvents(data.ut, [op], color)

# calendaridの全てのイベントを削除(成功したか返す)
def DeleteAllEvents(calendarid: str) -> bool:
  creds = CheckToken()
  try:
    service = build("calendar", "v3", credentials=creds)
    events = service.events().list(calendarId=calendarid).execute()["items"]
    for event in events:
      eventid = event['id']
      service.events().delete(calendarId=calendarid, eventId=eventid).execute()
      time.sleep(WAITING_TIME)
    return True
  except HttpError as error:
    return False

# 指定したイベントを削除 (成功したか返す)
def DeleteEvent(calendarid: str, event: map) -> bool:
  creds = CheckToken()
  try:
    service = build("calendar", "v3", credentials=creds)
    eventid = event['id']
    service.events().delete(calendarId=calendarid, eventId=eventid).execute()
    return True
  except HttpError as error:
    return False


登録するアドインのコードは以下のようになります。

# ImportSchedule.py
from CalendarAPI import *

def SelfRegistration(addIns: AIM.AddIns):
  addin = addIns.AddScript("ImportSchedule", int(GUI.AddInKeyType.AddInKeyMenuHelp))
  addin.MenuString = "予定取込み(自由カレンダー)"

def ImportSchedule(keyEntity: AIM.KeyEntity):
  data = FLEXSCHEData(keyEntity)
  ok = True
  for k, v in GetAllCalendars().items():
    ok &= ImportSchedulesAsFreeCalendars(data, v)
  data.project.FireEvent(GUI.FSEvent.FSEventFreeCalendarRecsAreUpdated)
  if ok:
    alert("取り込みに成功しました")
  else:
    alert("取り込みに失敗しました")
  return True

動作

以下のようなカレンダーがあります。
会議は作業員1と3、休みは作業員4が対象です

スクリーンショット 2025-08-07 102058.png 先ほどのアドインを実行してリスケすると、カレンダー上の予定を考慮して計画を立てることができました。 カレンダー前.png カレンダー後.png

逆にGoogleカレンダーに作業を反映することもできます。

アドイン:全作業をカレンダーに登録(クリックで展開)

# RegisterSchedule.py
from CalendarAPI import *

def SelfRegistration(addIns: AIM.AddIns):
  addin = addIns.AddScript("RegisterSchedule", int(GUI.AddInKeyType.AddInKeyMenuHelp))
  addin.MenuString = "全予定登録"
  addin.Order = 1

def RegisterSchedule(keyEntity: AIM.KeyEntity):
  data = FLEXSCHEData(keyEntity)
  opset = data.ds.OperationSet
  ops = []
  for i in range(opset.CountOfRecords):
    ops.append(opset.get_OperationRec(i))
  ok = AddAllEvents(data, ops, 1)
  if ok:
    alert("登録に成功しました")
  else:
    alert("登録に失敗しました")
  return True

送り返す2.png

様々なデバイスと連携

前章ではFLEXSCHEとGoogleカレンダーを連携させましたが、Googleカレンダーを介して様々なデバイスとも連携することができます。

IMG_0238.jpeg スマートウォッチと連携しました。 作業員それぞれが自身の予定だけ取り込むようにして、作業が変更されたときや開始直前に通知を受け取って作業を進めてもらうというような使い方ができます!

ユーザー

PAGETOP